advance.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. from typing import List, Optional
  2. from fastapi import APIRouter, Depends, HTTPException, status
  3. from sqlalchemy import or_, func
  4. from sqlalchemy.orm import Session
  5. from app.database import get_db
  6. from app.core.deps import get_current_user
  7. from app.models.user import User
  8. from app.models.advance import AdvanceApply
  9. from app.models.reimburse import ReimburseOffset
  10. from app.schemas.common import ApiResponse, PagedData
  11. from app.schemas.advance import (
  12. AdvanceApplyCreate,
  13. AdvanceApplyUpdate,
  14. AdvanceApplyOut,
  15. AdvanceApprovalCreate,
  16. AdvanceApprovalOut,
  17. AdvanceMyStatsOut,
  18. AdvanceApproverPendingStatsOut,
  19. )
  20. from app.services.advance import AdvanceService
  21. from app.services.expense_item import ExpenseItemService
  22. from app.models.workflow import WorkflowDef
  23. from app.utils.codegen import generate_apply_code
  24. from app.utils.date_range import parse_create_time_day_bounds
  25. router = APIRouter(prefix="/api/advances", tags=["备用金管理"])
  26. @router.get("/", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  27. def list_advances(
  28. page: int = 1,
  29. pageSize: int = 10,
  30. status: str | None = None,
  31. keyword: str = "",
  32. applicantId: Optional[int] = None,
  33. applyDate: Optional[str] = None,
  34. db: Session = Depends(get_db),
  35. current_user: User = Depends(get_current_user),
  36. ):
  37. filters = [AdvanceApply.del_flag == 0]
  38. if status:
  39. filters.append(AdvanceApply.status == status)
  40. if keyword:
  41. filters.append(or_(AdvanceApply.apply_code.contains(keyword), AdvanceApply.applicant_name.contains(keyword)))
  42. if applicantId is not None:
  43. filters.append(AdvanceApply.applicant_id == applicantId)
  44. bounds = parse_create_time_day_bounds(applyDate)
  45. if bounds:
  46. d0, d1 = bounds
  47. filters.append(AdvanceApply.create_time >= d0)
  48. filters.append(AdvanceApply.create_time < d1)
  49. rows, total = AdvanceService.get_paged(db, page=page, page_size=pageSize, filters=filters)
  50. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  51. if out_rows:
  52. offset_map = dict(
  53. db.query(ReimburseOffset.advance_id, func.coalesce(func.sum(ReimburseOffset.offset_amount), 0))
  54. .filter(ReimburseOffset.advance_id.in_([r.id for r in rows]))
  55. .group_by(ReimburseOffset.advance_id)
  56. .all()
  57. )
  58. for out_row in out_rows:
  59. out_row.offset_amount = float(offset_map.get(out_row.id, 0))
  60. paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)
  61. return ApiResponse(data=paged_data)
  62. @router.get("/my", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  63. def my_advances(
  64. page: int = 1,
  65. pageSize: int = 10,
  66. status: str | None = None,
  67. keyword: str = "",
  68. applyDate: Optional[str] = None,
  69. db: Session = Depends(get_db),
  70. current_user: User = Depends(get_current_user),
  71. ):
  72. filters = [AdvanceApply.del_flag == 0, AdvanceApply.applicant_id == current_user.id]
  73. if status:
  74. filters.append(AdvanceApply.status == status)
  75. if keyword:
  76. filters.append(or_(AdvanceApply.apply_code.contains(keyword), AdvanceApply.applicant_name.contains(keyword)))
  77. bounds = parse_create_time_day_bounds(applyDate)
  78. if bounds:
  79. d0, d1 = bounds
  80. filters.append(AdvanceApply.create_time >= d0)
  81. filters.append(AdvanceApply.create_time < d1)
  82. rows, total = AdvanceService.get_paged(db, page=page, page_size=pageSize, filters=filters)
  83. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  84. if out_rows:
  85. offset_map = dict(
  86. db.query(ReimburseOffset.advance_id, func.coalesce(func.sum(ReimburseOffset.offset_amount), 0))
  87. .filter(ReimburseOffset.advance_id.in_([r.id for r in rows]))
  88. .group_by(ReimburseOffset.advance_id)
  89. .all()
  90. )
  91. for out_row in out_rows:
  92. out_row.offset_amount = float(offset_map.get(out_row.id, 0))
  93. paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)
  94. return ApiResponse(data=paged_data)
  95. @router.get("/my/stats", response_model=ApiResponse[AdvanceMyStatsOut])
  96. def my_advance_stats(
  97. db: Session = Depends(get_db),
  98. current_user: User = Depends(get_current_user),
  99. ):
  100. data = AdvanceService.my_apply_stats(db, current_user.id)
  101. return ApiResponse(data=AdvanceMyStatsOut.model_validate(data))
  102. @router.get("/stats/status-counts", response_model=ApiResponse[dict])
  103. def advance_status_counts(
  104. db: Session = Depends(get_db),
  105. current_user: User = Depends(get_current_user),
  106. ):
  107. return ApiResponse(data=AdvanceService.global_status_counts(db))
  108. @router.get("/pending-approval", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  109. def pending_approval(
  110. page: int = 1,
  111. pageSize: int = 10,
  112. keyword: str = "",
  113. applicantId: Optional[int] = None,
  114. applyDate: Optional[str] = None,
  115. db: Session = Depends(get_db),
  116. current_user: User = Depends(get_current_user),
  117. ):
  118. rows, total = AdvanceService.pending_approval(
  119. db,
  120. current_user,
  121. page=page,
  122. page_size=pageSize,
  123. keyword=keyword,
  124. applicant_id=applicantId,
  125. apply_date=applyDate,
  126. )
  127. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  128. paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)
  129. return ApiResponse(data=paged_data)
  130. @router.get("/pending-approval/stats", response_model=ApiResponse[AdvanceApproverPendingStatsOut])
  131. def advance_pending_approval_stats(
  132. db: Session = Depends(get_db),
  133. current_user: User = Depends(get_current_user),
  134. ):
  135. data = AdvanceService.pending_approval_stats(db, current_user)
  136. return ApiResponse(data=AdvanceApproverPendingStatsOut.model_validate(data))
  137. @router.get("/approved-by-me", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  138. def approved_by_me(
  139. page: int = 1,
  140. pageSize: int = 10,
  141. keyword: str = "",
  142. applicantId: Optional[int] = None,
  143. applyDate: Optional[str] = None,
  144. db: Session = Depends(get_db),
  145. current_user: User = Depends(get_current_user),
  146. ):
  147. rows, total = AdvanceService.approved_by_me(
  148. db,
  149. current_user,
  150. page=page,
  151. page_size=pageSize,
  152. keyword=keyword,
  153. applicant_id=applicantId,
  154. apply_date=applyDate,
  155. )
  156. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  157. paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)
  158. return ApiResponse(data=paged_data)
  159. @router.get("/{advance_id}", response_model=ApiResponse[AdvanceApplyOut])
  160. def get_advance(advance_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
  161. advance = AdvanceService.get_by_id(db, advance_id)
  162. if not advance:
  163. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  164. out = AdvanceApplyOut.model_validate(advance)
  165. total_offset = (
  166. db.query(func.coalesce(func.sum(ReimburseOffset.offset_amount), 0))
  167. .filter(ReimburseOffset.advance_id == advance_id)
  168. .scalar()
  169. )
  170. out.offset_amount = float(total_offset or 0)
  171. return ApiResponse(data=out)
  172. @router.get("/{advance_id}/approvals", response_model=ApiResponse[List[AdvanceApprovalOut]])
  173. def get_approvals(advance_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
  174. approvals = AdvanceService.get_approvals(db, advance_id)
  175. return ApiResponse(data=[AdvanceApprovalOut.model_validate(a) for a in approvals])
  176. @router.post("/", response_model=ApiResponse[AdvanceApplyOut])
  177. def create_advance(body: AdvanceApplyCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
  178. data = body.model_dump(exclude_unset=True)
  179. eid = data.get("expense_item_id")
  180. if eid is not None:
  181. if not ExpenseItemService.get_by_id(db, int(eid)):
  182. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在")
  183. data["applicant_id"] = current_user.id
  184. data["applicant_name"] = current_user.real_name or current_user.username
  185. data["apply_code"] = generate_apply_code(db, "BYJ")
  186. data["status"] = "draft"
  187. wf = db.query(WorkflowDef).filter(WorkflowDef.biz_type == "advance", WorkflowDef.del_flag == 0).first()
  188. if wf:
  189. data["wf_id"] = wf.id
  190. advance = AdvanceService.create(db, data)
  191. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  192. @router.put("/{advance_id}", response_model=ApiResponse[AdvanceApplyOut])
  193. def update_advance(advance_id: int, body: AdvanceApplyUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
  194. data = body.model_dump(exclude_unset=True)
  195. if "expense_item_id" in data and data["expense_item_id"] is not None:
  196. eid = int(data["expense_item_id"])
  197. if not ExpenseItemService.get_by_id(db, eid):
  198. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在")
  199. advance = AdvanceService.update(db, advance_id, data)
  200. if not advance:
  201. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  202. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  203. @router.post("/{advance_id}/submit", response_model=ApiResponse[AdvanceApplyOut])
  204. def submit_advance(advance_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
  205. advance = AdvanceService.submit(db, advance_id, current_user)
  206. if not advance:
  207. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  208. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  209. @router.post("/{advance_id}/approve", response_model=ApiResponse[AdvanceApplyOut])
  210. def approve_advance(advance_id: int, body: AdvanceApprovalCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
  211. advance = AdvanceService.approve(db, advance_id, current_user, body.approval_opinion or "")
  212. if not advance:
  213. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  214. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  215. @router.post("/{advance_id}/reject", response_model=ApiResponse[AdvanceApplyOut])
  216. def reject_advance(advance_id: int, body: AdvanceApprovalCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
  217. advance = AdvanceService.reject(db, advance_id, current_user, body.approval_opinion or "")
  218. if not advance:
  219. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  220. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  221. @router.delete("/{advance_id}", response_model=ApiResponse[None])
  222. def delete_advance(advance_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
  223. AdvanceService.soft_delete(db, advance_id)
  224. return ApiResponse(data=None)