advance.py 25 KB

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