advance.py 13 KB

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