advance.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. from typing import List, Optional
  2. from fastapi import APIRouter, Depends, HTTPException, status
  3. from sqlalchemy import or_
  4. from sqlalchemy.orm import Session
  5. from app.core.bx_auth import BxUserContext, get_bx_current_user
  6. from app.core.database import get_db
  7. from app.core.tenant_context import get_tenant_no
  8. from app.models.bx_models import BxAdvanceApply, BxWorkflowDef
  9. from app.schemas.bx.advance import (
  10. AdvanceApplyCreate,
  11. AdvanceApplyOut,
  12. AdvanceApplyUpdate,
  13. AdvanceApprovalCreate,
  14. AdvanceApprovalOut,
  15. AdvanceApproverPendingStatsOut,
  16. AdvanceMyStatsOut,
  17. )
  18. from app.schemas.bx.common import ApiResponse, PagedData
  19. from app.services.bx.advance import AdvanceService
  20. from app.services.bx.expense_item import ExpenseItemService
  21. from app.utils.bx_codegen import generate_apply_code
  22. from app.utils.bx_date_range import append_create_time_filters
  23. router = APIRouter(prefix="/advances", tags=["备用金管理"])
  24. def _tenant(db: Session, user: BxUserContext) -> str:
  25. return get_tenant_no(db, user.to_tenant_user_dict())
  26. def _fill_offset_amounts(db: Session, tenant_no: str, rows, out_rows: List[AdvanceApplyOut]) -> None:
  27. if not out_rows:
  28. return
  29. offset_map = AdvanceService.offset_amount_map(db, tenant_no, [r.id for r in rows])
  30. for out_row in out_rows:
  31. out_row.offset_amount = offset_map.get(out_row.id, 0.0)
  32. @router.get("/", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  33. def list_advances(
  34. page: int = 1,
  35. pageSize: int = 10,
  36. status: Optional[str] = None,
  37. keyword: str = "",
  38. applicantId: Optional[str] = None,
  39. applyDate: Optional[str] = None,
  40. applyDateFrom: Optional[str] = None,
  41. applyDateTo: Optional[str] = None,
  42. db: Session = Depends(get_db),
  43. user: BxUserContext = Depends(get_bx_current_user),
  44. ):
  45. tenant_no = _tenant(db, user)
  46. filters = []
  47. if status:
  48. filters.append(BxAdvanceApply.status == status)
  49. if keyword:
  50. filters.append(
  51. or_(
  52. BxAdvanceApply.apply_code.contains(keyword),
  53. BxAdvanceApply.applicant_name.contains(keyword),
  54. )
  55. )
  56. if applicantId:
  57. filters.append(BxAdvanceApply.applicant_id == str(applicantId).strip())
  58. append_create_time_filters(
  59. filters,
  60. BxAdvanceApply,
  61. apply_date=applyDate,
  62. apply_date_from=applyDateFrom,
  63. apply_date_to=applyDateTo,
  64. )
  65. rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user)
  66. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  67. _fill_offset_amounts(db, tenant_no, rows, out_rows)
  68. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  69. @router.get("/my", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  70. def my_advances(
  71. page: int = 1,
  72. pageSize: int = 10,
  73. status: Optional[str] = None,
  74. keyword: str = "",
  75. applyDate: Optional[str] = None,
  76. applyDateFrom: Optional[str] = None,
  77. applyDateTo: Optional[str] = None,
  78. db: Session = Depends(get_db),
  79. user: BxUserContext = Depends(get_bx_current_user),
  80. ):
  81. tenant_no = _tenant(db, user)
  82. filters = [
  83. or_(
  84. BxAdvanceApply.applicant_id == user.id,
  85. BxAdvanceApply.create_by == user.id,
  86. )
  87. ]
  88. if status:
  89. filters.append(BxAdvanceApply.status == status)
  90. if keyword:
  91. filters.append(
  92. or_(
  93. BxAdvanceApply.apply_code.contains(keyword),
  94. BxAdvanceApply.applicant_name.contains(keyword),
  95. )
  96. )
  97. append_create_time_filters(
  98. filters,
  99. BxAdvanceApply,
  100. apply_date=applyDate,
  101. apply_date_from=applyDateFrom,
  102. apply_date_to=applyDateTo,
  103. )
  104. rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user)
  105. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  106. _fill_offset_amounts(db, tenant_no, rows, out_rows)
  107. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  108. @router.get("/my/stats", response_model=ApiResponse[AdvanceMyStatsOut])
  109. def my_advance_stats(
  110. db: Session = Depends(get_db),
  111. user: BxUserContext = Depends(get_bx_current_user),
  112. ):
  113. tenant_no = _tenant(db, user)
  114. data = AdvanceService.my_apply_stats(db, user, tenant_no)
  115. return ApiResponse(data=AdvanceMyStatsOut.model_validate(data))
  116. @router.get("/stats/status-counts", response_model=ApiResponse[dict])
  117. def advance_status_counts(
  118. db: Session = Depends(get_db),
  119. user: BxUserContext = Depends(get_bx_current_user),
  120. ):
  121. tenant_no = _tenant(db, user)
  122. return ApiResponse(data=AdvanceService.global_status_counts(db, tenant_no, user=user))
  123. @router.get("/pending-approval", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  124. def pending_approval(
  125. page: int = 1,
  126. pageSize: int = 10,
  127. keyword: str = "",
  128. applicantId: Optional[str] = None,
  129. applyDate: Optional[str] = None,
  130. applyDateFrom: Optional[str] = None,
  131. applyDateTo: Optional[str] = None,
  132. db: Session = Depends(get_db),
  133. user: BxUserContext = Depends(get_bx_current_user),
  134. ):
  135. tenant_no = _tenant(db, user)
  136. applicant_id = str(applicantId).strip() if applicantId else None
  137. rows, total = AdvanceService.pending_approval(
  138. db,
  139. user,
  140. tenant_no,
  141. page=page,
  142. page_size=pageSize,
  143. keyword=keyword,
  144. applicant_id=applicant_id,
  145. apply_date=applyDate,
  146. apply_date_from=applyDateFrom,
  147. apply_date_to=applyDateTo,
  148. )
  149. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  150. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  151. @router.get("/pending-approval/stats", response_model=ApiResponse[AdvanceApproverPendingStatsOut])
  152. def advance_pending_approval_stats(
  153. db: Session = Depends(get_db),
  154. user: BxUserContext = Depends(get_bx_current_user),
  155. ):
  156. tenant_no = _tenant(db, user)
  157. data = AdvanceService.pending_approval_stats(db, user, tenant_no)
  158. return ApiResponse(data=AdvanceApproverPendingStatsOut.model_validate(data))
  159. @router.get("/approved-by-me", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  160. def approved_by_me(
  161. page: int = 1,
  162. pageSize: int = 10,
  163. keyword: str = "",
  164. applicantId: Optional[str] = None,
  165. applyDate: Optional[str] = None,
  166. applyDateFrom: Optional[str] = None,
  167. applyDateTo: Optional[str] = None,
  168. db: Session = Depends(get_db),
  169. user: BxUserContext = Depends(get_bx_current_user),
  170. ):
  171. tenant_no = _tenant(db, user)
  172. applicant_id = str(applicantId).strip() if applicantId else None
  173. rows, total = AdvanceService.approved_by_me(
  174. db,
  175. user,
  176. tenant_no,
  177. page=page,
  178. page_size=pageSize,
  179. keyword=keyword,
  180. applicant_id=applicant_id,
  181. apply_date=applyDate,
  182. apply_date_from=applyDateFrom,
  183. apply_date_to=applyDateTo,
  184. )
  185. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  186. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  187. @router.get("/{advance_id}", response_model=ApiResponse[AdvanceApplyOut])
  188. def get_advance(
  189. advance_id: str,
  190. db: Session = Depends(get_db),
  191. user: BxUserContext = Depends(get_bx_current_user),
  192. ):
  193. tenant_no = _tenant(db, user)
  194. advance = AdvanceService.get_by_id(db, advance_id, tenant_no, user=user)
  195. if not advance:
  196. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  197. out = AdvanceApplyOut.model_validate(advance)
  198. offset_map = AdvanceService.offset_amount_map(db, tenant_no, [advance_id])
  199. out.offset_amount = offset_map.get(advance_id, 0.0)
  200. return ApiResponse(data=out)
  201. @router.get("/{advance_id}/approvals", response_model=ApiResponse[List[AdvanceApprovalOut]])
  202. def get_approvals(
  203. advance_id: str,
  204. db: Session = Depends(get_db),
  205. user: BxUserContext = Depends(get_bx_current_user),
  206. ):
  207. tenant_no = _tenant(db, user)
  208. if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user):
  209. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  210. approvals = AdvanceService.get_approvals(db, advance_id, tenant_no, user=user)
  211. return ApiResponse(data=[AdvanceApprovalOut.model_validate(a) for a in approvals])
  212. @router.get("/{advance_id}/approval-history", response_model=ApiResponse[List[AdvanceApprovalOut]])
  213. def get_approval_history(
  214. advance_id: str,
  215. db: Session = Depends(get_db),
  216. user: BxUserContext = Depends(get_bx_current_user),
  217. ):
  218. tenant_no = _tenant(db, user)
  219. if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user):
  220. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  221. history = AdvanceService.get_approval_history(db, advance_id, tenant_no, user=user)
  222. return ApiResponse(data=[AdvanceApprovalOut.model_validate(a) for a in history])
  223. @router.post("/", response_model=ApiResponse[AdvanceApplyOut])
  224. def create_advance(
  225. body: AdvanceApplyCreate,
  226. db: Session = Depends(get_db),
  227. user: BxUserContext = Depends(get_bx_current_user),
  228. ):
  229. tenant_no = _tenant(db, user)
  230. data = body.model_dump(exclude_unset=True)
  231. eid = data.get("expense_item_id")
  232. if eid:
  233. if not ExpenseItemService.get_by_id(db, eid, tenant_no, user=user, skip_scope=True):
  234. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在")
  235. data["applicant_id"] = user.id
  236. data["applicant_name"] = user.name
  237. data["apply_code"] = generate_apply_code(db, "BYJ", tenant_no)
  238. data["status"] = "draft"
  239. wf = (
  240. db.query(BxWorkflowDef)
  241. .filter(
  242. BxWorkflowDef.biz_type == "advance",
  243. BxWorkflowDef.tenant_no == tenant_no,
  244. BxWorkflowDef.del_flag == 0,
  245. )
  246. .first()
  247. )
  248. if wf:
  249. data["wf_id"] = wf.id
  250. advance = AdvanceService.create(db, data, user=user)
  251. db.commit()
  252. db.refresh(advance)
  253. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  254. @router.put("/{advance_id}", response_model=ApiResponse[AdvanceApplyOut])
  255. def update_advance(
  256. advance_id: str,
  257. body: AdvanceApplyUpdate,
  258. db: Session = Depends(get_db),
  259. user: BxUserContext = Depends(get_bx_current_user),
  260. ):
  261. tenant_no = _tenant(db, user)
  262. data = body.model_dump(exclude_unset=True)
  263. if data.get("expense_item_id"):
  264. eid = data["expense_item_id"]
  265. if not ExpenseItemService.get_by_id(db, eid, tenant_no, user=user, skip_scope=True):
  266. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在")
  267. advance = AdvanceService.update(db, advance_id, data, user=user)
  268. if not advance:
  269. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  270. db.commit()
  271. db.refresh(advance)
  272. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  273. @router.post("/{advance_id}/submit", response_model=ApiResponse[AdvanceApplyOut])
  274. def submit_advance(
  275. advance_id: str,
  276. db: Session = Depends(get_db),
  277. user: BxUserContext = Depends(get_bx_current_user),
  278. ):
  279. advance = AdvanceService.submit(db, advance_id, user)
  280. if not advance:
  281. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  282. db.commit()
  283. db.refresh(advance)
  284. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  285. @router.post("/{advance_id}/approve", response_model=ApiResponse[AdvanceApplyOut])
  286. def approve_advance(
  287. advance_id: str,
  288. body: AdvanceApprovalCreate,
  289. db: Session = Depends(get_db),
  290. user: BxUserContext = Depends(get_bx_current_user),
  291. ):
  292. advance = AdvanceService.approve(db, advance_id, user, body.approval_opinion or "")
  293. if not advance:
  294. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在或无权审批")
  295. db.commit()
  296. db.refresh(advance)
  297. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  298. @router.post("/{advance_id}/reject", response_model=ApiResponse[AdvanceApplyOut])
  299. def reject_advance(
  300. advance_id: str,
  301. body: AdvanceApprovalCreate,
  302. db: Session = Depends(get_db),
  303. user: BxUserContext = Depends(get_bx_current_user),
  304. ):
  305. advance = AdvanceService.reject(db, advance_id, user, body.approval_opinion or "")
  306. if not advance:
  307. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在或无权审批")
  308. db.commit()
  309. db.refresh(advance)
  310. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  311. @router.delete("/{advance_id}", response_model=ApiResponse[None])
  312. def delete_advance(
  313. advance_id: str,
  314. db: Session = Depends(get_db),
  315. user: BxUserContext = Depends(get_bx_current_user),
  316. ):
  317. tenant_no = _tenant(db, user)
  318. if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user):
  319. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  320. AdvanceService.soft_delete(db, advance_id, user=user)
  321. db.commit()
  322. return ApiResponse(data=None)