905 lines
29 KiB
Python
905 lines
29 KiB
Python
"""服务项目管理路由
|
|
|
|
实现服务项目的 CRUD 操作和成本管理
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy import select, func, or_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.database import get_db
|
|
from app.models import (
|
|
Project,
|
|
ProjectCostItem,
|
|
ProjectLaborCost,
|
|
ProjectCostSummary,
|
|
Category,
|
|
Material,
|
|
Equipment,
|
|
StaffLevel,
|
|
)
|
|
from app.schemas.common import ResponseModel, PaginatedData, ErrorCode
|
|
from app.schemas.project import (
|
|
ProjectCreate,
|
|
ProjectUpdate,
|
|
ProjectResponse,
|
|
ProjectListResponse,
|
|
CostSummaryBrief,
|
|
)
|
|
from app.schemas.project_cost import (
|
|
CostItemCreate,
|
|
CostItemUpdate,
|
|
CostItemResponse,
|
|
LaborCostCreate,
|
|
LaborCostUpdate,
|
|
LaborCostResponse,
|
|
CalculateCostRequest,
|
|
CostCalculationResult,
|
|
CostSummaryResponse,
|
|
ProjectDetailResponse,
|
|
CostItemType,
|
|
AllocationMethod,
|
|
)
|
|
from app.services.cost_service import CostService
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# 项目允许的排序字段白名单
|
|
PROJECT_SORT_FIELDS = {"created_at", "updated_at", "project_code", "project_name", "duration_minutes"}
|
|
|
|
|
|
# ============ 项目 CRUD ============
|
|
|
|
@router.get("", response_model=ResponseModel[PaginatedData[ProjectListResponse]])
|
|
async def get_projects(
|
|
page: int = Query(1, ge=1, description="页码"),
|
|
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
|
category_id: Optional[int] = Query(None, description="分类筛选"),
|
|
keyword: Optional[str] = Query(None, description="关键词搜索"),
|
|
is_active: Optional[bool] = Query(None, description="是否启用筛选"),
|
|
sort_by: str = Query("created_at", description="排序字段"),
|
|
sort_order: str = Query("desc", description="排序方向"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取服务项目列表"""
|
|
query = select(Project).options(
|
|
selectinload(Project.category),
|
|
selectinload(Project.cost_summary),
|
|
)
|
|
|
|
if keyword:
|
|
query = query.where(
|
|
or_(
|
|
Project.project_code.contains(keyword),
|
|
Project.project_name.contains(keyword),
|
|
)
|
|
)
|
|
if category_id:
|
|
query = query.where(Project.category_id == category_id)
|
|
if is_active is not None:
|
|
query = query.where(Project.is_active == is_active)
|
|
|
|
# 排序 - 使用白名单验证防止注入
|
|
if sort_by not in PROJECT_SORT_FIELDS:
|
|
sort_by = "created_at"
|
|
sort_column = getattr(Project, sort_by, Project.created_at)
|
|
if sort_order == "asc":
|
|
query = query.order_by(sort_column.asc())
|
|
else:
|
|
query = query.order_by(sort_column.desc())
|
|
|
|
# 分页
|
|
offset = (page - 1) * page_size
|
|
query = query.offset(offset).limit(page_size)
|
|
|
|
result = await db.execute(query)
|
|
projects = result.scalars().all()
|
|
|
|
# 统计总数
|
|
count_query = select(func.count(Project.id))
|
|
if keyword:
|
|
count_query = count_query.where(
|
|
or_(
|
|
Project.project_code.contains(keyword),
|
|
Project.project_name.contains(keyword),
|
|
)
|
|
)
|
|
if category_id:
|
|
count_query = count_query.where(Project.category_id == category_id)
|
|
if is_active is not None:
|
|
count_query = count_query.where(Project.is_active == is_active)
|
|
|
|
total_result = await db.execute(count_query)
|
|
total = total_result.scalar() or 0
|
|
|
|
# 构建响应
|
|
items = []
|
|
for p in projects:
|
|
item = {
|
|
"id": p.id,
|
|
"project_code": p.project_code,
|
|
"project_name": p.project_name,
|
|
"category_id": p.category_id,
|
|
"category_name": p.category.category_name if p.category else None,
|
|
"description": p.description,
|
|
"duration_minutes": p.duration_minutes,
|
|
"is_active": p.is_active,
|
|
"cost_summary": None,
|
|
"created_at": p.created_at,
|
|
"updated_at": p.updated_at,
|
|
}
|
|
if p.cost_summary:
|
|
item["cost_summary"] = CostSummaryBrief(
|
|
total_cost=float(p.cost_summary.total_cost),
|
|
material_cost=float(p.cost_summary.material_cost),
|
|
equipment_cost=float(p.cost_summary.equipment_cost),
|
|
labor_cost=float(p.cost_summary.labor_cost),
|
|
fixed_cost_allocation=float(p.cost_summary.fixed_cost_allocation),
|
|
)
|
|
items.append(ProjectListResponse(**item))
|
|
|
|
return ResponseModel(
|
|
data=PaginatedData(
|
|
items=items,
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size,
|
|
)
|
|
)
|
|
|
|
|
|
@router.get("/{project_id}", response_model=ResponseModel[ProjectDetailResponse])
|
|
async def get_project(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取项目详情(含成本明细)"""
|
|
result = await db.execute(
|
|
select(Project).options(
|
|
selectinload(Project.category),
|
|
selectinload(Project.cost_items),
|
|
selectinload(Project.labor_costs).selectinload(ProjectLaborCost.staff_level),
|
|
selectinload(Project.cost_summary),
|
|
).where(Project.id == project_id)
|
|
)
|
|
project = result.scalar_one_or_none()
|
|
|
|
if not project:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "项目不存在"}
|
|
)
|
|
|
|
# 构建成本明细响应
|
|
cost_items = []
|
|
for item in project.cost_items:
|
|
item_name = None
|
|
unit = None
|
|
if item.item_type == CostItemType.MATERIAL.value:
|
|
material_result = await db.execute(
|
|
select(Material).where(Material.id == item.item_id)
|
|
)
|
|
material = material_result.scalar_one_or_none()
|
|
if material:
|
|
item_name = material.material_name
|
|
unit = material.unit
|
|
else:
|
|
equipment_result = await db.execute(
|
|
select(Equipment).where(Equipment.id == item.item_id)
|
|
)
|
|
equipment = equipment_result.scalar_one_or_none()
|
|
if equipment:
|
|
item_name = equipment.equipment_name
|
|
unit = "次"
|
|
|
|
cost_items.append(CostItemResponse(
|
|
id=item.id,
|
|
item_type=CostItemType(item.item_type),
|
|
item_id=item.item_id,
|
|
item_name=item_name,
|
|
quantity=float(item.quantity),
|
|
unit=unit,
|
|
unit_cost=float(item.unit_cost),
|
|
total_cost=float(item.total_cost),
|
|
remark=item.remark,
|
|
created_at=item.created_at,
|
|
updated_at=item.updated_at,
|
|
))
|
|
|
|
# 构建人工成本响应
|
|
labor_costs = []
|
|
for item in project.labor_costs:
|
|
labor_costs.append(LaborCostResponse(
|
|
id=item.id,
|
|
staff_level_id=item.staff_level_id,
|
|
level_name=item.staff_level.level_name if item.staff_level else None,
|
|
duration_minutes=item.duration_minutes,
|
|
hourly_rate=float(item.hourly_rate),
|
|
labor_cost=float(item.labor_cost),
|
|
remark=item.remark,
|
|
created_at=item.created_at,
|
|
updated_at=item.updated_at,
|
|
))
|
|
|
|
# 构建成本汇总
|
|
cost_summary = None
|
|
if project.cost_summary:
|
|
cost_summary = CostSummaryResponse(
|
|
project_id=project.id,
|
|
material_cost=float(project.cost_summary.material_cost),
|
|
equipment_cost=float(project.cost_summary.equipment_cost),
|
|
labor_cost=float(project.cost_summary.labor_cost),
|
|
fixed_cost_allocation=float(project.cost_summary.fixed_cost_allocation),
|
|
total_cost=float(project.cost_summary.total_cost),
|
|
calculated_at=project.cost_summary.calculated_at,
|
|
)
|
|
|
|
return ResponseModel(
|
|
data=ProjectDetailResponse(
|
|
id=project.id,
|
|
project_code=project.project_code,
|
|
project_name=project.project_name,
|
|
category_id=project.category_id,
|
|
category_name=project.category.category_name if project.category else None,
|
|
description=project.description,
|
|
duration_minutes=project.duration_minutes,
|
|
is_active=project.is_active,
|
|
cost_items=cost_items,
|
|
labor_costs=labor_costs,
|
|
cost_summary=cost_summary,
|
|
created_at=project.created_at,
|
|
updated_at=project.updated_at,
|
|
)
|
|
)
|
|
|
|
|
|
@router.post("", response_model=ResponseModel[ProjectResponse])
|
|
async def create_project(
|
|
data: ProjectCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""创建服务项目"""
|
|
# 检查编码是否已存在
|
|
existing = await db.execute(
|
|
select(Project).where(Project.project_code == data.project_code)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"code": ErrorCode.ALREADY_EXISTS, "message": "项目编码已存在"}
|
|
)
|
|
|
|
# 检查分类是否存在
|
|
if data.category_id:
|
|
category_result = await db.execute(
|
|
select(Category).where(Category.id == data.category_id)
|
|
)
|
|
if not category_result.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "分类不存在"}
|
|
)
|
|
|
|
project = Project(**data.model_dump())
|
|
db.add(project)
|
|
await db.flush()
|
|
await db.refresh(project)
|
|
|
|
# 获取分类名称
|
|
category_name = None
|
|
if project.category_id:
|
|
result = await db.execute(
|
|
select(Category).where(Category.id == project.category_id)
|
|
)
|
|
category = result.scalar_one_or_none()
|
|
if category:
|
|
category_name = category.category_name
|
|
|
|
return ResponseModel(
|
|
message="创建成功",
|
|
data=ProjectResponse(
|
|
id=project.id,
|
|
project_code=project.project_code,
|
|
project_name=project.project_name,
|
|
category_id=project.category_id,
|
|
category_name=category_name,
|
|
description=project.description,
|
|
duration_minutes=project.duration_minutes,
|
|
is_active=project.is_active,
|
|
cost_summary=None,
|
|
created_at=project.created_at,
|
|
updated_at=project.updated_at,
|
|
)
|
|
)
|
|
|
|
|
|
@router.put("/{project_id}", response_model=ResponseModel[ProjectResponse])
|
|
async def update_project(
|
|
project_id: int,
|
|
data: ProjectUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""更新服务项目"""
|
|
result = await db.execute(
|
|
select(Project).options(
|
|
selectinload(Project.category),
|
|
selectinload(Project.cost_summary),
|
|
).where(Project.id == project_id)
|
|
)
|
|
project = result.scalar_one_or_none()
|
|
|
|
if not project:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "项目不存在"}
|
|
)
|
|
|
|
# 检查编码是否重复
|
|
if data.project_code and data.project_code != project.project_code:
|
|
existing = await db.execute(
|
|
select(Project).where(Project.project_code == data.project_code)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"code": ErrorCode.ALREADY_EXISTS, "message": "项目编码已存在"}
|
|
)
|
|
|
|
# 检查分类是否存在
|
|
if data.category_id:
|
|
category_result = await db.execute(
|
|
select(Category).where(Category.id == data.category_id)
|
|
)
|
|
if not category_result.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "分类不存在"}
|
|
)
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(project, field, value)
|
|
|
|
await db.flush()
|
|
await db.refresh(project)
|
|
|
|
# 获取分类名称
|
|
category_name = None
|
|
if project.category_id:
|
|
cat_result = await db.execute(
|
|
select(Category).where(Category.id == project.category_id)
|
|
)
|
|
category = cat_result.scalar_one_or_none()
|
|
if category:
|
|
category_name = category.category_name
|
|
|
|
# 构建成本汇总
|
|
cost_summary = None
|
|
if project.cost_summary:
|
|
cost_summary = CostSummaryBrief(
|
|
total_cost=float(project.cost_summary.total_cost),
|
|
material_cost=float(project.cost_summary.material_cost),
|
|
equipment_cost=float(project.cost_summary.equipment_cost),
|
|
labor_cost=float(project.cost_summary.labor_cost),
|
|
fixed_cost_allocation=float(project.cost_summary.fixed_cost_allocation),
|
|
)
|
|
|
|
return ResponseModel(
|
|
message="更新成功",
|
|
data=ProjectResponse(
|
|
id=project.id,
|
|
project_code=project.project_code,
|
|
project_name=project.project_name,
|
|
category_id=project.category_id,
|
|
category_name=category_name,
|
|
description=project.description,
|
|
duration_minutes=project.duration_minutes,
|
|
is_active=project.is_active,
|
|
cost_summary=cost_summary,
|
|
created_at=project.created_at,
|
|
updated_at=project.updated_at,
|
|
)
|
|
)
|
|
|
|
|
|
@router.delete("/{project_id}", response_model=ResponseModel)
|
|
async def delete_project(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""删除服务项目"""
|
|
result = await db.execute(select(Project).where(Project.id == project_id))
|
|
project = result.scalar_one_or_none()
|
|
|
|
if not project:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "项目不存在"}
|
|
)
|
|
|
|
await db.delete(project)
|
|
|
|
return ResponseModel(message="删除成功")
|
|
|
|
|
|
# ============ 成本明细(耗材/设备)管理 ============
|
|
|
|
@router.get("/{project_id}/cost-items", response_model=ResponseModel[list[CostItemResponse]])
|
|
async def get_cost_items(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取项目成本明细"""
|
|
# 检查项目是否存在
|
|
project_result = await db.execute(
|
|
select(Project).where(Project.id == project_id)
|
|
)
|
|
if not project_result.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "项目不存在"}
|
|
)
|
|
|
|
result = await db.execute(
|
|
select(ProjectCostItem).where(
|
|
ProjectCostItem.project_id == project_id
|
|
).order_by(ProjectCostItem.id)
|
|
)
|
|
items = result.scalars().all()
|
|
|
|
response_items = []
|
|
for item in items:
|
|
item_name = None
|
|
unit = None
|
|
if item.item_type == CostItemType.MATERIAL.value:
|
|
material_result = await db.execute(
|
|
select(Material).where(Material.id == item.item_id)
|
|
)
|
|
material = material_result.scalar_one_or_none()
|
|
if material:
|
|
item_name = material.material_name
|
|
unit = material.unit
|
|
else:
|
|
equipment_result = await db.execute(
|
|
select(Equipment).where(Equipment.id == item.item_id)
|
|
)
|
|
equipment = equipment_result.scalar_one_or_none()
|
|
if equipment:
|
|
item_name = equipment.equipment_name
|
|
unit = "次"
|
|
|
|
response_items.append(CostItemResponse(
|
|
id=item.id,
|
|
item_type=CostItemType(item.item_type),
|
|
item_id=item.item_id,
|
|
item_name=item_name,
|
|
quantity=float(item.quantity),
|
|
unit=unit,
|
|
unit_cost=float(item.unit_cost),
|
|
total_cost=float(item.total_cost),
|
|
remark=item.remark,
|
|
created_at=item.created_at,
|
|
updated_at=item.updated_at,
|
|
))
|
|
|
|
return ResponseModel(data=response_items)
|
|
|
|
|
|
@router.post("/{project_id}/cost-items", response_model=ResponseModel[CostItemResponse])
|
|
async def create_cost_item(
|
|
project_id: int,
|
|
data: CostItemCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""添加成本明细"""
|
|
# 检查项目是否存在
|
|
project_result = await db.execute(
|
|
select(Project).where(Project.id == project_id)
|
|
)
|
|
if not project_result.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "项目不存在"}
|
|
)
|
|
|
|
cost_service = CostService(db)
|
|
|
|
try:
|
|
cost_item = await cost_service.add_cost_item(
|
|
project_id=project_id,
|
|
item_type=data.item_type,
|
|
item_id=data.item_id,
|
|
quantity=data.quantity,
|
|
remark=data.remark,
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": str(e)}
|
|
)
|
|
|
|
# 获取物品名称
|
|
item_name = None
|
|
unit = None
|
|
if data.item_type == CostItemType.MATERIAL:
|
|
material_result = await db.execute(
|
|
select(Material).where(Material.id == data.item_id)
|
|
)
|
|
material = material_result.scalar_one_or_none()
|
|
if material:
|
|
item_name = material.material_name
|
|
unit = material.unit
|
|
else:
|
|
equipment_result = await db.execute(
|
|
select(Equipment).where(Equipment.id == data.item_id)
|
|
)
|
|
equipment = equipment_result.scalar_one_or_none()
|
|
if equipment:
|
|
item_name = equipment.equipment_name
|
|
unit = "次"
|
|
|
|
return ResponseModel(
|
|
message="添加成功",
|
|
data=CostItemResponse(
|
|
id=cost_item.id,
|
|
item_type=CostItemType(cost_item.item_type),
|
|
item_id=cost_item.item_id,
|
|
item_name=item_name,
|
|
quantity=float(cost_item.quantity),
|
|
unit=unit,
|
|
unit_cost=float(cost_item.unit_cost),
|
|
total_cost=float(cost_item.total_cost),
|
|
remark=cost_item.remark,
|
|
created_at=cost_item.created_at,
|
|
updated_at=cost_item.updated_at,
|
|
)
|
|
)
|
|
|
|
|
|
@router.put("/{project_id}/cost-items/{item_id}", response_model=ResponseModel[CostItemResponse])
|
|
async def update_cost_item(
|
|
project_id: int,
|
|
item_id: int,
|
|
data: CostItemUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""更新成本明细"""
|
|
result = await db.execute(
|
|
select(ProjectCostItem).where(
|
|
ProjectCostItem.id == item_id,
|
|
ProjectCostItem.project_id == project_id,
|
|
)
|
|
)
|
|
cost_item = result.scalar_one_or_none()
|
|
|
|
if not cost_item:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "成本明细不存在"}
|
|
)
|
|
|
|
cost_service = CostService(db)
|
|
cost_item = await cost_service.update_cost_item(
|
|
cost_item=cost_item,
|
|
quantity=data.quantity,
|
|
remark=data.remark,
|
|
)
|
|
|
|
# 获取物品名称
|
|
item_name = None
|
|
unit = None
|
|
if cost_item.item_type == CostItemType.MATERIAL.value:
|
|
material_result = await db.execute(
|
|
select(Material).where(Material.id == cost_item.item_id)
|
|
)
|
|
material = material_result.scalar_one_or_none()
|
|
if material:
|
|
item_name = material.material_name
|
|
unit = material.unit
|
|
else:
|
|
equipment_result = await db.execute(
|
|
select(Equipment).where(Equipment.id == cost_item.item_id)
|
|
)
|
|
equipment = equipment_result.scalar_one_or_none()
|
|
if equipment:
|
|
item_name = equipment.equipment_name
|
|
unit = "次"
|
|
|
|
return ResponseModel(
|
|
message="更新成功",
|
|
data=CostItemResponse(
|
|
id=cost_item.id,
|
|
item_type=CostItemType(cost_item.item_type),
|
|
item_id=cost_item.item_id,
|
|
item_name=item_name,
|
|
quantity=float(cost_item.quantity),
|
|
unit=unit,
|
|
unit_cost=float(cost_item.unit_cost),
|
|
total_cost=float(cost_item.total_cost),
|
|
remark=cost_item.remark,
|
|
created_at=cost_item.created_at,
|
|
updated_at=cost_item.updated_at,
|
|
)
|
|
)
|
|
|
|
|
|
@router.delete("/{project_id}/cost-items/{item_id}", response_model=ResponseModel)
|
|
async def delete_cost_item(
|
|
project_id: int,
|
|
item_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""删除成本明细"""
|
|
result = await db.execute(
|
|
select(ProjectCostItem).where(
|
|
ProjectCostItem.id == item_id,
|
|
ProjectCostItem.project_id == project_id,
|
|
)
|
|
)
|
|
cost_item = result.scalar_one_or_none()
|
|
|
|
if not cost_item:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "成本明细不存在"}
|
|
)
|
|
|
|
await db.delete(cost_item)
|
|
|
|
return ResponseModel(message="删除成功")
|
|
|
|
|
|
# ============ 人工成本管理 ============
|
|
|
|
@router.get("/{project_id}/labor-costs", response_model=ResponseModel[list[LaborCostResponse]])
|
|
async def get_labor_costs(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取项目人工成本"""
|
|
# 检查项目是否存在
|
|
project_result = await db.execute(
|
|
select(Project).where(Project.id == project_id)
|
|
)
|
|
if not project_result.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "项目不存在"}
|
|
)
|
|
|
|
result = await db.execute(
|
|
select(ProjectLaborCost).options(
|
|
selectinload(ProjectLaborCost.staff_level)
|
|
).where(
|
|
ProjectLaborCost.project_id == project_id
|
|
).order_by(ProjectLaborCost.id)
|
|
)
|
|
items = result.scalars().all()
|
|
|
|
response_items = []
|
|
for item in items:
|
|
response_items.append(LaborCostResponse(
|
|
id=item.id,
|
|
staff_level_id=item.staff_level_id,
|
|
level_name=item.staff_level.level_name if item.staff_level else None,
|
|
duration_minutes=item.duration_minutes,
|
|
hourly_rate=float(item.hourly_rate),
|
|
labor_cost=float(item.labor_cost),
|
|
remark=item.remark,
|
|
created_at=item.created_at,
|
|
updated_at=item.updated_at,
|
|
))
|
|
|
|
return ResponseModel(data=response_items)
|
|
|
|
|
|
@router.post("/{project_id}/labor-costs", response_model=ResponseModel[LaborCostResponse])
|
|
async def create_labor_cost(
|
|
project_id: int,
|
|
data: LaborCostCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""添加人工成本"""
|
|
# 检查项目是否存在
|
|
project_result = await db.execute(
|
|
select(Project).where(Project.id == project_id)
|
|
)
|
|
if not project_result.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "项目不存在"}
|
|
)
|
|
|
|
cost_service = CostService(db)
|
|
|
|
try:
|
|
labor_cost = await cost_service.add_labor_cost(
|
|
project_id=project_id,
|
|
staff_level_id=data.staff_level_id,
|
|
duration_minutes=data.duration_minutes,
|
|
remark=data.remark,
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": str(e)}
|
|
)
|
|
|
|
# 获取级别名称
|
|
level_result = await db.execute(
|
|
select(StaffLevel).where(StaffLevel.id == data.staff_level_id)
|
|
)
|
|
staff_level = level_result.scalar_one_or_none()
|
|
|
|
return ResponseModel(
|
|
message="添加成功",
|
|
data=LaborCostResponse(
|
|
id=labor_cost.id,
|
|
staff_level_id=labor_cost.staff_level_id,
|
|
level_name=staff_level.level_name if staff_level else None,
|
|
duration_minutes=labor_cost.duration_minutes,
|
|
hourly_rate=float(labor_cost.hourly_rate),
|
|
labor_cost=float(labor_cost.labor_cost),
|
|
remark=labor_cost.remark,
|
|
created_at=labor_cost.created_at,
|
|
updated_at=labor_cost.updated_at,
|
|
)
|
|
)
|
|
|
|
|
|
@router.put("/{project_id}/labor-costs/{item_id}", response_model=ResponseModel[LaborCostResponse])
|
|
async def update_labor_cost(
|
|
project_id: int,
|
|
item_id: int,
|
|
data: LaborCostUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""更新人工成本"""
|
|
result = await db.execute(
|
|
select(ProjectLaborCost).where(
|
|
ProjectLaborCost.id == item_id,
|
|
ProjectLaborCost.project_id == project_id,
|
|
)
|
|
)
|
|
labor_item = result.scalar_one_or_none()
|
|
|
|
if not labor_item:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "人工成本记录不存在"}
|
|
)
|
|
|
|
cost_service = CostService(db)
|
|
|
|
try:
|
|
labor_item = await cost_service.update_labor_cost(
|
|
labor_item=labor_item,
|
|
staff_level_id=data.staff_level_id,
|
|
duration_minutes=data.duration_minutes,
|
|
remark=data.remark,
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": str(e)}
|
|
)
|
|
|
|
# 获取级别名称
|
|
level_result = await db.execute(
|
|
select(StaffLevel).where(StaffLevel.id == labor_item.staff_level_id)
|
|
)
|
|
staff_level = level_result.scalar_one_or_none()
|
|
|
|
return ResponseModel(
|
|
message="更新成功",
|
|
data=LaborCostResponse(
|
|
id=labor_item.id,
|
|
staff_level_id=labor_item.staff_level_id,
|
|
level_name=staff_level.level_name if staff_level else None,
|
|
duration_minutes=labor_item.duration_minutes,
|
|
hourly_rate=float(labor_item.hourly_rate),
|
|
labor_cost=float(labor_item.labor_cost),
|
|
remark=labor_item.remark,
|
|
created_at=labor_item.created_at,
|
|
updated_at=labor_item.updated_at,
|
|
)
|
|
)
|
|
|
|
|
|
@router.delete("/{project_id}/labor-costs/{item_id}", response_model=ResponseModel)
|
|
async def delete_labor_cost(
|
|
project_id: int,
|
|
item_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""删除人工成本"""
|
|
result = await db.execute(
|
|
select(ProjectLaborCost).where(
|
|
ProjectLaborCost.id == item_id,
|
|
ProjectLaborCost.project_id == project_id,
|
|
)
|
|
)
|
|
labor_item = result.scalar_one_or_none()
|
|
|
|
if not labor_item:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "人工成本记录不存在"}
|
|
)
|
|
|
|
await db.delete(labor_item)
|
|
|
|
return ResponseModel(message="删除成功")
|
|
|
|
|
|
# ============ 成本计算 ============
|
|
|
|
@router.post("/{project_id}/calculate-cost", response_model=ResponseModel[CostCalculationResult])
|
|
async def calculate_cost(
|
|
project_id: int,
|
|
data: CalculateCostRequest = CalculateCostRequest(),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""计算项目总成本"""
|
|
cost_service = CostService(db)
|
|
|
|
try:
|
|
result = await cost_service.calculate_project_cost(
|
|
project_id=project_id,
|
|
allocation_method=data.fixed_cost_allocation_method,
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": str(e)}
|
|
)
|
|
|
|
return ResponseModel(message="计算完成", data=result)
|
|
|
|
|
|
@router.get("/{project_id}/cost-summary", response_model=ResponseModel[CostSummaryResponse])
|
|
async def get_cost_summary(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取成本汇总"""
|
|
# 检查项目是否存在
|
|
project_result = await db.execute(
|
|
select(Project).where(Project.id == project_id)
|
|
)
|
|
if not project_result.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "项目不存在"}
|
|
)
|
|
|
|
result = await db.execute(
|
|
select(ProjectCostSummary).where(
|
|
ProjectCostSummary.project_id == project_id
|
|
)
|
|
)
|
|
summary = result.scalar_one_or_none()
|
|
|
|
if not summary:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"code": ErrorCode.NOT_FOUND, "message": "成本汇总不存在,请先计算成本"}
|
|
)
|
|
|
|
return ResponseModel(
|
|
data=CostSummaryResponse(
|
|
project_id=summary.project_id,
|
|
material_cost=float(summary.material_cost),
|
|
equipment_cost=float(summary.equipment_cost),
|
|
labor_cost=float(summary.labor_cost),
|
|
fixed_cost_allocation=float(summary.fixed_cost_allocation),
|
|
total_cost=float(summary.total_cost),
|
|
calculated_at=summary.calculated_at,
|
|
)
|
|
)
|