advance.py 22 KB

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