from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import or_ from sqlalchemy.orm import Session from app.database import get_db from app.core.deps import get_current_user from app.models.user import User from app.models.reimburse import ReimburseApply from app.schemas.common import ApiResponse, PagedData from app.schemas.reimburse import ( ReimburseApplyCreate, ReimburseApplyUpdate, ReimburseApplyOut, ReimburseApprovalCreate, ReimburseApprovalOut, ReimburseMyStatsOut, ReimburseApproverPendingStatsOut, ) from app.services.reimburse import ReimburseService from app.services.expense_item import ExpenseItemService from app.models.workflow import WorkflowDef from app.utils.codegen import generate_apply_code, generate_title from app.utils.date_range import parse_create_time_day_bounds, resolve_apply_create_time router = APIRouter(prefix="/api/reimburses", tags=["报销管理"]) def _build_out_with_details(reimburse: ReimburseApply, db: Session) -> ReimburseApplyOut: return ReimburseService.build_apply_out_with_details(db, reimburse) @router.get("/generate-title", response_model=ApiResponse[str]) def get_generated_title( applyDate: Optional[str] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): title = generate_title(db, current_user.real_name or current_user.username, applyDate) return ApiResponse(data=title) @router.get("/", response_model=ApiResponse[PagedData[ReimburseApplyOut]]) def list_reimburses( page: int = 1, pageSize: int = 10, status: str | None = None, keyword: str = "", expense_id: int | None = None, applicantId: Optional[int] = None, applyDate: Optional[str] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): filters = [ReimburseApply.del_flag == 0] if status: filters.append(ReimburseApply.status == status) if keyword: filters.append(or_(ReimburseApply.apply_code.contains(keyword), ReimburseApply.applicant_name.contains(keyword))) if expense_id: filters.append(ReimburseApply.expense_id == expense_id) if applicantId is not None: filters.append(ReimburseApply.applicant_id == applicantId) bounds = parse_create_time_day_bounds(applyDate) if bounds: d0, d1 = bounds filters.append(ReimburseApply.create_time >= d0) filters.append(ReimburseApply.create_time < d1) rows, total = ReimburseService.get_paged(db, page=page, page_size=pageSize, filters=filters) out_rows = ReimburseService.out_rows_with_attachment_counts(db, rows) paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize) return ApiResponse(data=paged_data) @router.get("/my", response_model=ApiResponse[PagedData[ReimburseApplyOut]]) def my_reimburses( page: int = 1, pageSize: int = 10, status: str | None = None, keyword: str = "", applyDate: Optional[str] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): filters = [ReimburseApply.del_flag == 0, ReimburseApply.applicant_id == current_user.id] if status: filters.append(ReimburseApply.status == status) if keyword: filters.append(or_(ReimburseApply.apply_code.contains(keyword), ReimburseApply.applicant_name.contains(keyword))) bounds = parse_create_time_day_bounds(applyDate) if bounds: d0, d1 = bounds filters.append(ReimburseApply.create_time >= d0) filters.append(ReimburseApply.create_time < d1) rows, total = ReimburseService.get_paged(db, page=page, page_size=pageSize, filters=filters) out_rows = ReimburseService.out_rows_with_attachment_counts(db, rows) paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize) return ApiResponse(data=paged_data) @router.get("/my/stats", response_model=ApiResponse[ReimburseMyStatsOut]) def my_reimburse_stats( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): data = ReimburseService.my_apply_stats(db, current_user.id) return ApiResponse(data=ReimburseMyStatsOut.model_validate(data)) @router.get("/stats/status-counts", response_model=ApiResponse[dict]) def reimburse_status_counts( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """全库报销单各状态数量(与列表页「全部单据」汇总一致,不受分页影响)。""" return ApiResponse(data=ReimburseService.global_status_counts(db)) @router.get("/pending-approval", response_model=ApiResponse[PagedData[ReimburseApplyOut]]) def pending_approval( page: int = 1, pageSize: int = 10, keyword: str = "", applicantId: Optional[int] = None, applyDate: Optional[str] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): rows, total = ReimburseService.pending_approval( db, current_user, page=page, page_size=pageSize, keyword=keyword, applicant_id=applicantId, apply_date=applyDate, ) out_rows = ReimburseService.out_rows_with_attachment_counts(db, rows) paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize) return ApiResponse(data=paged_data) @router.get("/pending-approval/stats", response_model=ApiResponse[ReimburseApproverPendingStatsOut]) def pending_approval_stats( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): data = ReimburseService.pending_approval_stats(db, current_user) return ApiResponse(data=ReimburseApproverPendingStatsOut.model_validate(data)) @router.get("/approved-by-me", response_model=ApiResponse[PagedData[ReimburseApplyOut]]) def approved_by_me( page: int = 1, pageSize: int = 10, keyword: str = "", applicantId: Optional[int] = None, applyDate: Optional[str] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): rows, total = ReimburseService.approved_by_me( db, current_user, page=page, page_size=pageSize, keyword=keyword, applicant_id=applicantId, apply_date=applyDate, ) out_rows = ReimburseService.out_rows_with_attachment_counts(db, rows) paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize) return ApiResponse(data=paged_data) @router.get("/{reimburse_id}", response_model=ApiResponse[ReimburseApplyOut]) def get_reimburse(reimburse_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): reimburse = ReimburseService.get_by_id(db, reimburse_id) if not reimburse: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在") out = _build_out_with_details(reimburse, db) return ApiResponse(data=out) @router.get("/{reimburse_id}/approvals", response_model=ApiResponse[List[ReimburseApprovalOut]]) def get_approvals(reimburse_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): approvals = ReimburseService.get_approvals(db, reimburse_id) return ApiResponse(data=[ReimburseApprovalOut.model_validate(a) for a in approvals]) @router.post("/", response_model=ApiResponse[ReimburseApplyOut]) def create_reimburse(body: ReimburseApplyCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): details_data = [] for d in body.details: item = ExpenseItemService.get_by_id(db, d.expense_item_id) if not item: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {d.expense_item_id} 不存在", ) details_data.append({ "expense_id": d.expense_item_id, "expense_name": item.item_name, "amount": d.amount, "remark": d.remark, }) apply_date = body.apply_date data = { "apply_code": generate_apply_code(db, "BX", apply_date), "status": "draft", "remark": body.remark, "apply_date": apply_date, "create_time": resolve_apply_create_time(apply_date), "update_time": resolve_apply_create_time(apply_date), } if body.title: data["title"] = body.title wf = db.query(WorkflowDef).filter(WorkflowDef.biz_type == "reimburse", WorkflowDef.del_flag == 0).first() if wf: data["wf_id"] = wf.id reimburse = ReimburseService.create_with_details(db, data, details_data, current_user) out = _build_out_with_details(reimburse, db) return ApiResponse(data=out) @router.put("/{reimburse_id}", response_model=ApiResponse[ReimburseApplyOut]) def update_reimburse(reimburse_id: int, body: ReimburseApplyUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): details_data = None if body.details is not None: details_data = [] for d in body.details: item = ExpenseItemService.get_by_id(db, d.expense_item_id) if not item: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {d.expense_item_id} 不存在", ) details_data.append({ "expense_id": d.expense_item_id, "expense_name": item.item_name, "amount": d.amount, "remark": d.remark, }) main_data = {} if body.remark is not None: main_data["remark"] = body.remark reimburse = ReimburseService.update_with_details(db, reimburse_id, main_data, details_data, current_user) if not reimburse: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在") out = _build_out_with_details(reimburse, db) return ApiResponse(data=out) @router.post("/{reimburse_id}/submit", response_model=ApiResponse[ReimburseApplyOut]) def submit_reimburse(reimburse_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): reimburse = ReimburseService.submit(db, reimburse_id, current_user) if not reimburse: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在") return ApiResponse(data=ReimburseService.out_one_with_attachment_counts(db, reimburse)) @router.post("/{reimburse_id}/approve", response_model=ApiResponse[ReimburseApplyOut]) def approve_reimburse(reimburse_id: int, body: ReimburseApprovalCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): reimburse = ReimburseService.approve(db, reimburse_id, current_user, body.approval_opinion or "") if not reimburse: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在") return ApiResponse(data=ReimburseService.out_one_with_attachment_counts(db, reimburse)) @router.post("/{reimburse_id}/reject", response_model=ApiResponse[ReimburseApplyOut]) def reject_reimburse(reimburse_id: int, body: ReimburseApprovalCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): reimburse = ReimburseService.reject(db, reimburse_id, current_user, body.approval_opinion or "") if not reimburse: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在") return ApiResponse(data=ReimburseService.out_one_with_attachment_counts(db, reimburse)) @router.delete("/{reimburse_id}", response_model=ApiResponse[None]) def delete_reimburse(reimburse_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): ReimburseService.soft_delete(db, reimburse_id) return ApiResponse(data=None)