advance.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. from typing import Any, List, Optional
  2. from fastapi import APIRouter, Depends, HTTPException, Request, status, Query
  3. from sqlalchemy import or_
  4. from sqlalchemy.orm import Session
  5. from app.api.bx.export_common import (
  6. EXPORT_MAX_ROWS,
  7. ExportColumn,
  8. ExportValidationError,
  9. BxApplyListExportBody,
  10. dump_export_row,
  11. respond_xlsx,
  12. )
  13. from app.core.bx_auth import BxUserContext, get_bx_current_user
  14. from app.core.database import get_db
  15. from app.core.tenant_context import get_tenant_no
  16. from app.models.bx_models import BxAdvanceApply, BxWorkflowDef
  17. from app.schemas.bx.advance import (
  18. AdvanceApplyCreate,
  19. AdvanceApplyOut,
  20. AdvanceApplyUpdate,
  21. AdvanceApprovalCreate,
  22. AdvanceApprovalOut,
  23. AdvanceApproverPendingStatsOut,
  24. AdvanceMyStatsOut,
  25. )
  26. from app.schemas.bx.common import ApiResponse, PagedData
  27. from app.services.bx.advance import AdvanceService
  28. from app.services.bx.advance_list_filters import build_advance_column_filters
  29. from app.services.bx.expense_item import ExpenseItemService
  30. from app.services.bx.list_sort import order_advance, parse_sort_params
  31. router = APIRouter(prefix="/advances", tags=["备用金管理"])
  32. def _tenant(db: Session, user: BxUserContext) -> str:
  33. return get_tenant_no(db, user.to_tenant_user_dict())
  34. def _advance_order(params: dict):
  35. sf, so = parse_sort_params(params)
  36. return order_advance(sf, so)
  37. def _fill_offset_amounts(db: Session, tenant_no: str, rows, out_rows: List[AdvanceApplyOut]) -> None:
  38. if not out_rows:
  39. return
  40. offset_map = AdvanceService.offset_amount_map(db, tenant_no, [r.id for r in rows])
  41. for out_row in out_rows:
  42. out_row.offset_amount = offset_map.get(out_row.id, 0.0)
  43. ADVANCE_EXPORT_COLUMNS = [
  44. ExportColumn(title="申请单号", field="applyCode"),
  45. ExportColumn(title="申请时间", field="applyTime"),
  46. ExportColumn(title="申请人", field="applicantName"),
  47. ExportColumn(title="申请原因", field="reason"),
  48. ExportColumn(title="申请金额(元)", field="amount"),
  49. ExportColumn(title="实际发放金额(元)", field="actualAmount"),
  50. ExportColumn(title="冲抵总金额(元)", field="offsetAmount"),
  51. ExportColumn(title="状态", field="status"),
  52. ExportColumn(title="当前节点", field="currentNode"),
  53. ]
  54. def _merge_query(request: Request, **known) -> dict:
  55. out = {k: v for k, v in known.items() if v is not None and v != ""}
  56. for k, v in request.query_params.multi_items():
  57. if k in ("page", "pageSize"):
  58. continue
  59. if k not in out and v not in (None, ""):
  60. out[k] = v
  61. return out
  62. def _param_get(params: dict, *keys: str) -> Any:
  63. for key in keys:
  64. if key in params and params[key] not in (None, ""):
  65. return params[key]
  66. return None
  67. def _filters_from_params(params: dict, *, tenant_no: str = "") -> list:
  68. ids = _param_get(params, "ids")
  69. if isinstance(ids, str):
  70. ids = [s.strip() for s in ids.split(",") if s.strip()]
  71. return build_advance_column_filters(
  72. tenant_no=tenant_no,
  73. keyword=str(_param_get(params, "keyword") or ""),
  74. status=_param_get(params, "status"),
  75. status_in=_param_get(params, "statusIn", "status_in"),
  76. apply_code=_param_get(params, "applyCode", "apply_code"),
  77. reason=_param_get(params, "reason"),
  78. applicant_id=_param_get(params, "applicantId", "applicant_id"),
  79. applicant_name=_param_get(params, "applicantName", "applicant_name"),
  80. current_node=_param_get(params, "currentNode", "current_node"),
  81. issue_by=_param_get(params, "issueBy", "issue_by"),
  82. issue_user_id=_param_get(params, "issueUserId", "issue_user_id"),
  83. issue_user_name=_param_get(params, "issueUserName", "issue_user_name"),
  84. amount_op=_param_get(params, "amountOp", "amount_op"),
  85. amount=_param_get(params, "amount"),
  86. amount_min=_param_get(params, "amountMin", "amount_min"),
  87. amount_max=_param_get(params, "amountMax", "amount_max"),
  88. actual_amount_op=_param_get(params, "actualAmountOp", "actual_amount_op"),
  89. actual_amount=_param_get(params, "actualAmount", "actual_amount"),
  90. actual_amount_min=_param_get(params, "actualAmountMin", "actual_amount_min"),
  91. actual_amount_max=_param_get(params, "actualAmountMax", "actual_amount_max"),
  92. offset_amount_op=_param_get(params, "offsetAmountOp", "offset_amount_op"),
  93. offset_amount=_param_get(params, "offsetAmount", "offset_amount"),
  94. offset_amount_min=_param_get(params, "offsetAmountMin", "offset_amount_min"),
  95. offset_amount_max=_param_get(params, "offsetAmountMax", "offset_amount_max"),
  96. apply_date=_param_get(params, "applyDate", "apply_date"),
  97. apply_date_from=_param_get(params, "applyDateFrom", "apply_date_from"),
  98. apply_date_to=_param_get(params, "applyDateTo", "apply_date_to"),
  99. issue_time_from=_param_get(params, "issueTimeFrom", "issue_time_from"),
  100. issue_time_to=_param_get(params, "issueTimeTo", "issue_time_to"),
  101. ids=ids if isinstance(ids, list) else None,
  102. )
  103. def _body_to_params(body: BxApplyListExportBody) -> dict:
  104. data = body.model_dump(by_alias=True, exclude_none=True)
  105. extra = getattr(body, "model_extra", None) or {}
  106. for key, value in extra.items():
  107. if key not in data and value not in (None, ""):
  108. data[key] = value
  109. return data
  110. def _advance_list_filters(body: BxApplyListExportBody, *, tenant_no: str = "") -> list:
  111. return _filters_from_params(_body_to_params(body), tenant_no=tenant_no)
  112. def _export_advance_rows(
  113. db: Session,
  114. tenant_no: str,
  115. rows,
  116. *,
  117. with_offset: bool = True,
  118. ) -> List[dict]:
  119. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  120. if with_offset:
  121. _fill_offset_amounts(db, tenant_no, rows, out_rows)
  122. return [dump_export_row(r) for r in out_rows]
  123. @router.get("/", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  124. def list_advances(
  125. request: Request,
  126. page: int = 1,
  127. pageSize: int = Query(10, ge=1, le=500),
  128. status: Optional[str] = None,
  129. keyword: str = "",
  130. applicantId: Optional[str] = None,
  131. applyDate: Optional[str] = None,
  132. applyDateFrom: Optional[str] = None,
  133. applyDateTo: Optional[str] = None,
  134. db: Session = Depends(get_db),
  135. user: BxUserContext = Depends(get_bx_current_user),
  136. ):
  137. tenant_no = _tenant(db, user)
  138. params = _merge_query(
  139. request,
  140. status=status,
  141. keyword=keyword or None,
  142. applicantId=applicantId,
  143. applyDate=applyDate,
  144. applyDateFrom=applyDateFrom,
  145. applyDateTo=applyDateTo,
  146. )
  147. filters = _filters_from_params(params, tenant_no=tenant_no)
  148. rows, total = AdvanceService.get_paged(
  149. db,
  150. tenant_no,
  151. page=page,
  152. page_size=pageSize,
  153. filters=filters,
  154. user=user,
  155. order_by=_advance_order(params),
  156. )
  157. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  158. _fill_offset_amounts(db, tenant_no, rows, out_rows)
  159. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  160. @router.get("/my", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  161. def my_advances(
  162. request: Request,
  163. page: int = 1,
  164. pageSize: int = Query(10, ge=1, le=500),
  165. status: Optional[str] = None,
  166. keyword: str = "",
  167. applyDate: Optional[str] = None,
  168. applyDateFrom: Optional[str] = None,
  169. applyDateTo: Optional[str] = None,
  170. db: Session = Depends(get_db),
  171. user: BxUserContext = Depends(get_bx_current_user),
  172. ):
  173. tenant_no = _tenant(db, user)
  174. params = _merge_query(
  175. request,
  176. status=status,
  177. keyword=keyword or None,
  178. applyDate=applyDate,
  179. applyDateFrom=applyDateFrom,
  180. applyDateTo=applyDateTo,
  181. )
  182. filters = [
  183. or_(
  184. BxAdvanceApply.applicant_id == user.id,
  185. BxAdvanceApply.create_by == user.id,
  186. )
  187. ]
  188. filters.extend(_filters_from_params(params, tenant_no=tenant_no))
  189. rows, total = AdvanceService.get_paged(
  190. db,
  191. tenant_no,
  192. page=page,
  193. page_size=pageSize,
  194. filters=filters,
  195. user=user,
  196. order_by=_advance_order(params),
  197. )
  198. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  199. _fill_offset_amounts(db, tenant_no, rows, out_rows)
  200. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  201. @router.get("/my/stats", response_model=ApiResponse[AdvanceMyStatsOut])
  202. def my_advance_stats(
  203. db: Session = Depends(get_db),
  204. user: BxUserContext = Depends(get_bx_current_user),
  205. ):
  206. tenant_no = _tenant(db, user)
  207. data = AdvanceService.my_apply_stats(db, user, tenant_no)
  208. return ApiResponse(data=AdvanceMyStatsOut.model_validate(data))
  209. @router.get("/stats/status-counts", response_model=ApiResponse[dict])
  210. def advance_status_counts(
  211. db: Session = Depends(get_db),
  212. user: BxUserContext = Depends(get_bx_current_user),
  213. ):
  214. tenant_no = _tenant(db, user)
  215. return ApiResponse(data=AdvanceService.global_status_counts(db, tenant_no, user=user))
  216. @router.get("/pending-approval", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  217. def pending_approval(
  218. request: Request,
  219. page: int = 1,
  220. pageSize: int = Query(10, ge=1, le=500),
  221. keyword: str = "",
  222. applicantId: Optional[str] = None,
  223. applyDate: Optional[str] = None,
  224. applyDateFrom: Optional[str] = None,
  225. applyDateTo: Optional[str] = None,
  226. db: Session = Depends(get_db),
  227. user: BxUserContext = Depends(get_bx_current_user),
  228. ):
  229. tenant_no = _tenant(db, user)
  230. params = _merge_query(
  231. request,
  232. keyword=keyword or None,
  233. applicantId=applicantId,
  234. applyDate=applyDate,
  235. applyDateFrom=applyDateFrom,
  236. applyDateTo=applyDateTo,
  237. )
  238. # Tab 语义固定 pending;不把 status 列参传入 builder
  239. params.pop("status", None)
  240. params.pop("statusIn", None)
  241. params.pop("status_in", None)
  242. extra_filters = _filters_from_params(params, tenant_no=tenant_no)
  243. rows, total = AdvanceService.pending_approval(
  244. db,
  245. user,
  246. tenant_no,
  247. page=page,
  248. page_size=pageSize,
  249. extra_filters=extra_filters,
  250. order_by=_advance_order(params),
  251. )
  252. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  253. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  254. @router.get("/pending-approval/stats", response_model=ApiResponse[AdvanceApproverPendingStatsOut])
  255. def advance_pending_approval_stats(
  256. db: Session = Depends(get_db),
  257. user: BxUserContext = Depends(get_bx_current_user),
  258. ):
  259. tenant_no = _tenant(db, user)
  260. data = AdvanceService.pending_approval_stats(db, user, tenant_no)
  261. return ApiResponse(data=AdvanceApproverPendingStatsOut.model_validate(data))
  262. @router.get("/approved-by-me", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
  263. def approved_by_me(
  264. request: Request,
  265. page: int = 1,
  266. pageSize: int = Query(10, ge=1, le=500),
  267. keyword: str = "",
  268. applicantId: Optional[str] = None,
  269. applyDate: Optional[str] = None,
  270. applyDateFrom: Optional[str] = None,
  271. applyDateTo: Optional[str] = None,
  272. db: Session = Depends(get_db),
  273. user: BxUserContext = Depends(get_bx_current_user),
  274. ):
  275. tenant_no = _tenant(db, user)
  276. params = _merge_query(
  277. request,
  278. keyword=keyword or None,
  279. applicantId=applicantId,
  280. applyDate=applyDate,
  281. applyDateFrom=applyDateFrom,
  282. applyDateTo=applyDateTo,
  283. )
  284. extra_filters = _filters_from_params(params, tenant_no=tenant_no)
  285. rows, total = AdvanceService.approved_by_me(
  286. db,
  287. user,
  288. tenant_no,
  289. page=page,
  290. page_size=pageSize,
  291. extra_filters=extra_filters,
  292. order_by=_advance_order(params),
  293. )
  294. out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
  295. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  296. @router.post("/export")
  297. def export_advances(
  298. body: BxApplyListExportBody,
  299. db: Session = Depends(get_db),
  300. user: BxUserContext = Depends(get_bx_current_user),
  301. ):
  302. try:
  303. tenant_no = _tenant(db, user)
  304. params = _body_to_params(body)
  305. filters = _filters_from_params(params, tenant_no=tenant_no)
  306. rows, _ = AdvanceService.get_paged(
  307. db,
  308. tenant_no,
  309. page=1,
  310. page_size=EXPORT_MAX_ROWS,
  311. filters=filters,
  312. user=user,
  313. order_by=_advance_order(params),
  314. )
  315. export_rows = _export_advance_rows(db, tenant_no, rows)
  316. return respond_xlsx(
  317. db,
  318. tenant_no=tenant_no,
  319. sheet_title="备用金申请",
  320. columns=ADVANCE_EXPORT_COLUMNS,
  321. rows=export_rows,
  322. filename_stem="备用金申请",
  323. requested_columns=body.columns,
  324. )
  325. except ExportValidationError as e:
  326. raise HTTPException(status_code=400, detail=str(e)) from e
  327. @router.post("/my/export")
  328. def export_my_advances(
  329. body: BxApplyListExportBody,
  330. db: Session = Depends(get_db),
  331. user: BxUserContext = Depends(get_bx_current_user),
  332. ):
  333. try:
  334. tenant_no = _tenant(db, user)
  335. params = _body_to_params(body)
  336. filters = [
  337. or_(
  338. BxAdvanceApply.applicant_id == user.id,
  339. BxAdvanceApply.create_by == user.id,
  340. )
  341. ]
  342. filters.extend(_filters_from_params(params, tenant_no=tenant_no))
  343. rows, _ = AdvanceService.get_paged(
  344. db,
  345. tenant_no,
  346. page=1,
  347. page_size=EXPORT_MAX_ROWS,
  348. filters=filters,
  349. user=user,
  350. order_by=_advance_order(params),
  351. )
  352. export_rows = _export_advance_rows(db, tenant_no, rows)
  353. return respond_xlsx(
  354. db,
  355. tenant_no=tenant_no,
  356. sheet_title="我的备用金",
  357. columns=ADVANCE_EXPORT_COLUMNS,
  358. rows=export_rows,
  359. filename_stem="我的备用金申请",
  360. requested_columns=body.columns,
  361. )
  362. except ExportValidationError as e:
  363. raise HTTPException(status_code=400, detail=str(e)) from e
  364. @router.post("/pending-approval/export")
  365. def export_pending_approval_advances(
  366. body: BxApplyListExportBody,
  367. db: Session = Depends(get_db),
  368. user: BxUserContext = Depends(get_bx_current_user),
  369. ):
  370. try:
  371. tenant_no = _tenant(db, user)
  372. params = _body_to_params(body)
  373. params.pop("status", None)
  374. params.pop("statusIn", None)
  375. params.pop("status_in", None)
  376. extra_filters = _filters_from_params(params, tenant_no=tenant_no)
  377. rows, _ = AdvanceService.pending_approval(
  378. db,
  379. user,
  380. tenant_no,
  381. page=1,
  382. page_size=EXPORT_MAX_ROWS,
  383. extra_filters=extra_filters,
  384. order_by=_advance_order(params),
  385. )
  386. export_rows = _export_advance_rows(db, tenant_no, rows, with_offset=False)
  387. return respond_xlsx(
  388. db,
  389. tenant_no=tenant_no,
  390. sheet_title="备用金待审批",
  391. columns=ADVANCE_EXPORT_COLUMNS,
  392. rows=export_rows,
  393. filename_stem="备用金待审批",
  394. requested_columns=body.columns,
  395. )
  396. except ExportValidationError as e:
  397. raise HTTPException(status_code=400, detail=str(e)) from e
  398. @router.post("/approved-by-me/export")
  399. def export_approved_by_me_advances(
  400. body: BxApplyListExportBody,
  401. db: Session = Depends(get_db),
  402. user: BxUserContext = Depends(get_bx_current_user),
  403. ):
  404. try:
  405. tenant_no = _tenant(db, user)
  406. params = _body_to_params(body)
  407. extra_filters = _filters_from_params(params, tenant_no=tenant_no)
  408. rows, _ = AdvanceService.approved_by_me(
  409. db,
  410. user,
  411. tenant_no,
  412. page=1,
  413. page_size=EXPORT_MAX_ROWS,
  414. extra_filters=extra_filters,
  415. order_by=_advance_order(params),
  416. )
  417. export_rows = _export_advance_rows(db, tenant_no, rows, with_offset=False)
  418. return respond_xlsx(
  419. db,
  420. tenant_no=tenant_no,
  421. sheet_title="备用金已审批",
  422. columns=ADVANCE_EXPORT_COLUMNS,
  423. rows=export_rows,
  424. filename_stem="备用金已审批",
  425. requested_columns=body.columns,
  426. )
  427. except ExportValidationError as e:
  428. raise HTTPException(status_code=400, detail=str(e)) from e
  429. @router.get("/{advance_id}", response_model=ApiResponse[AdvanceApplyOut])
  430. def get_advance(
  431. advance_id: str,
  432. db: Session = Depends(get_db),
  433. user: BxUserContext = Depends(get_bx_current_user),
  434. ):
  435. tenant_no = _tenant(db, user)
  436. advance = AdvanceService.get_by_id(db, advance_id, tenant_no, user=user)
  437. if not advance:
  438. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  439. out = AdvanceApplyOut.model_validate(advance)
  440. offset_map = AdvanceService.offset_amount_map(db, tenant_no, [advance_id])
  441. out.offset_amount = offset_map.get(advance_id, 0.0)
  442. return ApiResponse(data=out)
  443. @router.get("/{advance_id}/approvals", response_model=ApiResponse[List[AdvanceApprovalOut]])
  444. def get_approvals(
  445. advance_id: str,
  446. db: Session = Depends(get_db),
  447. user: BxUserContext = Depends(get_bx_current_user),
  448. ):
  449. tenant_no = _tenant(db, user)
  450. if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user):
  451. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  452. approvals = AdvanceService.get_approvals(db, advance_id, tenant_no, user=user)
  453. return ApiResponse(data=[AdvanceApprovalOut.model_validate(a) for a in approvals])
  454. @router.get("/{advance_id}/approval-history", response_model=ApiResponse[List[AdvanceApprovalOut]])
  455. def get_approval_history(
  456. advance_id: str,
  457. db: Session = Depends(get_db),
  458. user: BxUserContext = Depends(get_bx_current_user),
  459. ):
  460. tenant_no = _tenant(db, user)
  461. if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user):
  462. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  463. history = AdvanceService.get_approval_history(db, advance_id, tenant_no, user=user)
  464. return ApiResponse(data=[AdvanceApprovalOut.model_validate(a) for a in history])
  465. @router.post("/", response_model=ApiResponse[AdvanceApplyOut])
  466. def create_advance(
  467. body: AdvanceApplyCreate,
  468. db: Session = Depends(get_db),
  469. user: BxUserContext = Depends(get_bx_current_user),
  470. ):
  471. tenant_no = _tenant(db, user)
  472. data = body.model_dump(exclude_unset=True)
  473. eid = data.get("expense_item_id")
  474. if eid:
  475. if not ExpenseItemService.get_by_id(db, eid, tenant_no, user=user, skip_scope=True):
  476. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在")
  477. data["applicant_id"] = user.id
  478. data["applicant_name"] = user.name
  479. data["status"] = "draft"
  480. wf = (
  481. db.query(BxWorkflowDef)
  482. .filter(
  483. BxWorkflowDef.biz_type == "advance",
  484. BxWorkflowDef.tenant_no == tenant_no,
  485. BxWorkflowDef.del_flag == 0,
  486. )
  487. .first()
  488. )
  489. if wf:
  490. data["wf_id"] = wf.id
  491. advance = AdvanceService.create(db, data, user=user)
  492. db.commit()
  493. db.refresh(advance)
  494. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  495. @router.put("/{advance_id}", response_model=ApiResponse[AdvanceApplyOut])
  496. def update_advance(
  497. advance_id: str,
  498. body: AdvanceApplyUpdate,
  499. db: Session = Depends(get_db),
  500. user: BxUserContext = Depends(get_bx_current_user),
  501. ):
  502. tenant_no = _tenant(db, user)
  503. data = body.model_dump(exclude_unset=True)
  504. if data.get("expense_item_id"):
  505. eid = data["expense_item_id"]
  506. if not ExpenseItemService.get_by_id(db, eid, tenant_no, user=user, skip_scope=True):
  507. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在")
  508. advance = AdvanceService.update(db, advance_id, data, user=user)
  509. if not advance:
  510. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  511. db.commit()
  512. db.refresh(advance)
  513. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  514. @router.post("/{advance_id}/submit", response_model=ApiResponse[AdvanceApplyOut])
  515. def submit_advance(
  516. advance_id: str,
  517. db: Session = Depends(get_db),
  518. user: BxUserContext = Depends(get_bx_current_user),
  519. ):
  520. advance = AdvanceService.submit(db, advance_id, user)
  521. if not advance:
  522. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  523. db.commit()
  524. db.refresh(advance)
  525. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  526. @router.post("/{advance_id}/approve", response_model=ApiResponse[AdvanceApplyOut])
  527. def approve_advance(
  528. advance_id: str,
  529. body: AdvanceApprovalCreate,
  530. db: Session = Depends(get_db),
  531. user: BxUserContext = Depends(get_bx_current_user),
  532. ):
  533. advance = AdvanceService.approve(db, advance_id, user, body.approval_opinion or "")
  534. if not advance:
  535. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在或无权审批")
  536. db.commit()
  537. db.refresh(advance)
  538. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  539. @router.post("/{advance_id}/reject", response_model=ApiResponse[AdvanceApplyOut])
  540. def reject_advance(
  541. advance_id: str,
  542. body: AdvanceApprovalCreate,
  543. db: Session = Depends(get_db),
  544. user: BxUserContext = Depends(get_bx_current_user),
  545. ):
  546. advance = AdvanceService.reject(db, advance_id, user, body.approval_opinion or "")
  547. if not advance:
  548. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在或无权审批")
  549. db.commit()
  550. db.refresh(advance)
  551. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  552. @router.post("/{advance_id}/withdraw", response_model=ApiResponse[AdvanceApplyOut])
  553. def withdraw_advance(
  554. advance_id: str,
  555. db: Session = Depends(get_db),
  556. user: BxUserContext = Depends(get_bx_current_user),
  557. ):
  558. advance = AdvanceService.withdraw(db, advance_id, user)
  559. if not advance:
  560. raise HTTPException(
  561. status_code=status.HTTP_400_BAD_REQUEST,
  562. detail="备用金申请不存在、非本人申请或当前状态不可撤回",
  563. )
  564. db.commit()
  565. db.refresh(advance)
  566. return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
  567. @router.delete("/{advance_id}", response_model=ApiResponse[None])
  568. def delete_advance(
  569. advance_id: str,
  570. db: Session = Depends(get_db),
  571. user: BxUserContext = Depends(get_bx_current_user),
  572. ):
  573. tenant_no = _tenant(db, user)
  574. if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user):
  575. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
  576. AdvanceService.soft_delete(db, advance_id, user=user)
  577. db.commit()
  578. return ApiResponse(data=None)