5278190750
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务 - 后端: 成本分析服务、AI问答服务 - 后端: 科目映射CRUD API、分析API、QA API - 后端: 集成测试(认证/任务/凭证) 49个测试全部通过 - 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面 - 前端: AuthGuard认证守卫、Dashboard AI聊天功能 - 前端: Playwright E2E测试 16 passed, 1 skipped - 基础设施: Docker Compose、Nginx反向代理、.env.example - 文档: 用户手册、管理员手册、发布检查清单
242 lines
7.6 KiB
Python
242 lines
7.6 KiB
Python
"""
|
|
成本分析 API
|
|
|
|
提供人工成本分析、部门拆分、费用科目拆分、环比变化等接口
|
|
"""
|
|
|
|
import csv
|
|
import io
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.core.tenant import get_current_company_id
|
|
from app.services.analysis.cost_calculator import CostCalculatorService
|
|
from app.services.ai.cost_analyzer import CostAnalyzerService
|
|
|
|
router = APIRouter(prefix="/api/analysis", tags=["成本分析"])
|
|
|
|
|
|
class CostSummaryResponse(BaseModel):
|
|
"""成本汇总响应"""
|
|
total_cost: float
|
|
salary_cost: float
|
|
social_security_cost: float
|
|
fund_cost: float
|
|
employee_count: int
|
|
|
|
|
|
class DepartmentCostResponse(BaseModel):
|
|
"""部门成本响应"""
|
|
department: str
|
|
employee_count: int
|
|
salary_cost: float
|
|
social_security_cost: float
|
|
fund_cost: float
|
|
total_cost: float
|
|
|
|
|
|
class ExpenseBreakdownResponse(BaseModel):
|
|
"""费用科目拆分响应"""
|
|
expense_type: str
|
|
amount: float
|
|
|
|
|
|
class MonthOverMonthResponse(BaseModel):
|
|
"""环比变化响应"""
|
|
total_cost: Dict[str, Any]
|
|
salary_cost: Dict[str, Any]
|
|
social_security_cost: Dict[str, Any]
|
|
fund_cost: Dict[str, Any]
|
|
employee_count: Dict[str, Any]
|
|
|
|
|
|
class CostChangeAnalysisResponse(BaseModel):
|
|
"""成本变化分析响应"""
|
|
new_employees: List[Dict[str, Any]]
|
|
left_employees: List[Dict[str, Any]]
|
|
salary_adjustments: List[Dict[str, Any]]
|
|
new_employee_cost: float
|
|
left_employee_saving: float
|
|
adjustment_cost: float
|
|
net_change: float
|
|
|
|
|
|
class FullAnalysisResponse(BaseModel):
|
|
"""完整成本分析响应"""
|
|
summary: CostSummaryResponse
|
|
departments: List[DepartmentCostResponse]
|
|
expenses: List[ExpenseBreakdownResponse]
|
|
changes: Optional[Dict[str, Any]] = None
|
|
ai_summary: Optional[str] = None
|
|
|
|
|
|
@router.get("/labor-cost/{task_id}", response_model=FullAnalysisResponse)
|
|
async def get_labor_cost_analysis(
|
|
task_id: int,
|
|
prev_task_id: Optional[int] = Query(None, description="上月任务ID,用于环比"),
|
|
db: AsyncSession = Depends(get_db),
|
|
company_id: int = Depends(get_current_company_id),
|
|
) -> FullAnalysisResponse:
|
|
"""
|
|
获取人工成本分析
|
|
|
|
包含总额、部门拆分、费用科目拆分,可选环比
|
|
"""
|
|
service = CostCalculatorService(db)
|
|
|
|
summary = await service.calculate_total_cost(task_id)
|
|
departments = await service.calculate_by_department(task_id)
|
|
expenses = await service.calculate_by_expense_type(task_id)
|
|
|
|
changes = None
|
|
ai_summary = None
|
|
|
|
if prev_task_id:
|
|
changes = await service.calculate_month_over_month(task_id, prev_task_id)
|
|
change_detail = await service.analyze_cost_changes(task_id, prev_task_id)
|
|
|
|
# 尝试 AI 分析
|
|
try:
|
|
analyzer = CostAnalyzerService()
|
|
prev_summary = await service.calculate_total_cost(prev_task_id)
|
|
ai_summary = await analyzer.analyze_cost_changes(
|
|
summary.to_dict(),
|
|
prev_summary.to_dict(),
|
|
change_detail,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
return FullAnalysisResponse(
|
|
summary=CostSummaryResponse(**summary.to_dict()),
|
|
departments=[
|
|
DepartmentCostResponse(**d.to_dict()) for d in departments
|
|
],
|
|
expenses=[
|
|
ExpenseBreakdownResponse(expense_type=k, amount=v)
|
|
for k, v in expenses.items()
|
|
],
|
|
changes=changes,
|
|
ai_summary=ai_summary,
|
|
)
|
|
|
|
|
|
@router.get("/labor-cost/{task_id}/summary", response_model=CostSummaryResponse)
|
|
async def get_cost_summary(
|
|
task_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
company_id: int = Depends(get_current_company_id),
|
|
) -> CostSummaryResponse:
|
|
"""获取成本汇总"""
|
|
service = CostCalculatorService(db)
|
|
summary = await service.calculate_total_cost(task_id)
|
|
return CostSummaryResponse(**summary.to_dict())
|
|
|
|
|
|
@router.get("/labor-cost/{task_id}/departments", response_model=List[DepartmentCostResponse])
|
|
async def get_department_costs(
|
|
task_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
company_id: int = Depends(get_current_company_id),
|
|
) -> List[DepartmentCostResponse]:
|
|
"""获取部门成本拆分"""
|
|
service = CostCalculatorService(db)
|
|
departments = await service.calculate_by_department(task_id)
|
|
return [DepartmentCostResponse(**d.to_dict()) for d in departments]
|
|
|
|
|
|
@router.get("/labor-cost/{task_id}/expenses", response_model=List[ExpenseBreakdownResponse])
|
|
async def get_expense_breakdown(
|
|
task_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
company_id: int = Depends(get_current_company_id),
|
|
) -> List[ExpenseBreakdownResponse]:
|
|
"""获取费用科目拆分"""
|
|
service = CostCalculatorService(db)
|
|
expenses = await service.calculate_by_expense_type(task_id)
|
|
return [
|
|
ExpenseBreakdownResponse(expense_type=k, amount=v)
|
|
for k, v in expenses.items()
|
|
]
|
|
|
|
|
|
@router.get("/labor-cost/{task_id}/changes", response_model=CostChangeAnalysisResponse)
|
|
async def get_cost_changes(
|
|
task_id: int,
|
|
prev_task_id: int = Query(..., description="上月任务ID"),
|
|
db: AsyncSession = Depends(get_db),
|
|
company_id: int = Depends(get_current_company_id),
|
|
) -> CostChangeAnalysisResponse:
|
|
"""获取成本变化分析"""
|
|
service = CostCalculatorService(db)
|
|
changes = await service.analyze_cost_changes(task_id, prev_task_id)
|
|
return CostChangeAnalysisResponse(**changes)
|
|
|
|
|
|
@router.get("/labor-cost/{task_id}/export")
|
|
async def export_cost_analysis(
|
|
task_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
company_id: int = Depends(get_current_company_id),
|
|
) -> StreamingResponse:
|
|
"""
|
|
导出成本分析为 Excel
|
|
|
|
返回包含成本汇总、部门拆分、费用拆分的 Excel 文件
|
|
"""
|
|
service = CostCalculatorService(db)
|
|
summary = await service.calculate_total_cost(task_id)
|
|
departments = await service.calculate_by_department(task_id)
|
|
expenses = await service.calculate_by_expense_type(task_id)
|
|
|
|
# 使用 openpyxl 生成 Excel
|
|
from openpyxl import Workbook
|
|
|
|
wb = Workbook()
|
|
|
|
# Sheet1: 成本汇总
|
|
ws1 = wb.active
|
|
ws1.title = "成本汇总"
|
|
ws1.append(["项目", "金额(元)"])
|
|
ws1.append(["人工成本总额", summary.total_cost])
|
|
ws1.append(["工资成本", summary.salary_cost])
|
|
ws1.append(["社保成本(公司部分)", summary.social_security_cost])
|
|
ws1.append(["公积金成本(公司部分)", summary.fund_cost])
|
|
ws1.append(["员工人数", summary.employee_count])
|
|
|
|
# Sheet2: 部门拆分
|
|
ws2 = wb.create_sheet("部门拆分")
|
|
ws2.append(["部门", "人数", "工资成本", "社保成本", "公积金成本", "合计"])
|
|
for dept in departments:
|
|
ws2.append([
|
|
dept.department,
|
|
dept.employee_count,
|
|
dept.salary_cost,
|
|
dept.social_security_cost,
|
|
dept.fund_cost,
|
|
dept.total_cost,
|
|
])
|
|
|
|
# Sheet3: 费用科目拆分
|
|
ws3 = wb.create_sheet("费用科目拆分")
|
|
ws3.append(["费用科目", "金额(元)"])
|
|
for expense_type, amount in expenses.items():
|
|
ws3.append([expense_type, amount])
|
|
|
|
# 输出到内存
|
|
output = io.BytesIO()
|
|
wb.save(output)
|
|
output.seek(0)
|
|
|
|
filename = f"cost_analysis_{task_id}.xlsx"
|
|
return StreamingResponse(
|
|
output,
|
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|