advance.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. from __future__ import annotations
  2. from datetime import datetime
  3. from typing import Any, Dict, List, Optional, Sequence, Tuple
  4. from sqlalchemy import and_, func, or_
  5. from sqlalchemy.orm import Session
  6. from app.core.bx_auth import BxUserContext
  7. from app.core.tenant_context import get_tenant_no
  8. from app.core.utils import generate_uuid
  9. from app.models.bx_models import (
  10. BxAdvanceApply,
  11. BxAdvanceApproval,
  12. BxReimburseOffset,
  13. BxWorkflowDef,
  14. BxWorkflowNode,
  15. )
  16. from app.services.bx.approval_cycle import filter_current_cycle_approvals
  17. from app.services.bx.base import BxCrudService
  18. from app.services.bx_data_scope import apply_bx_scope
  19. from app.utils.bx_codegen import generate_apply_code
  20. from app.utils.bx_date_range import append_create_time_filters, current_month_bounds, resolve_create_time_bounds
  21. class AdvanceService:
  22. @staticmethod
  23. def _tenant_no(db: Session, user: BxUserContext) -> str:
  24. return get_tenant_no(db, user.to_tenant_user_dict())
  25. @staticmethod
  26. def get_by_id(
  27. db: Session,
  28. advance_id: str,
  29. tenant_no: str,
  30. *,
  31. user: Optional[BxUserContext] = None,
  32. skip_scope: bool = False,
  33. ) -> Optional[BxAdvanceApply]:
  34. return BxCrudService.get_by_id(
  35. BxAdvanceApply, db, advance_id, tenant_no, user=user, skip_scope=skip_scope
  36. )
  37. @staticmethod
  38. def get_paged(
  39. db: Session,
  40. tenant_no: str,
  41. page: int,
  42. page_size: int,
  43. filters: Optional[List[Any]] = None,
  44. *,
  45. user: Optional[BxUserContext] = None,
  46. order_by: Optional[Any] = None,
  47. ) -> Tuple[Sequence[BxAdvanceApply], int]:
  48. return BxCrudService.get_paged(
  49. BxAdvanceApply,
  50. db,
  51. tenant_no,
  52. page,
  53. page_size,
  54. filters,
  55. user=user,
  56. order_by=order_by,
  57. )
  58. @staticmethod
  59. def create(
  60. db: Session,
  61. obj_in: dict,
  62. *,
  63. user: BxUserContext,
  64. ) -> BxAdvanceApply:
  65. from sqlalchemy.exc import IntegrityError
  66. tenant_no = AdvanceService._tenant_no(db, user)
  67. data = dict(obj_in)
  68. data.pop("apply_code", None)
  69. last_error: Exception | None = None
  70. for attempt in range(5):
  71. payload = {
  72. **data,
  73. "apply_code": generate_apply_code(db, "BYJ", tenant_no),
  74. }
  75. try:
  76. return BxCrudService.create(
  77. BxAdvanceApply,
  78. db,
  79. payload,
  80. tenant_no=tenant_no,
  81. user_id=user.id,
  82. dept_id=user.dept_id,
  83. )
  84. except IntegrityError as exc:
  85. db.rollback()
  86. last_error = exc
  87. if attempt >= 4:
  88. raise
  89. continue
  90. if last_error:
  91. raise last_error
  92. raise RuntimeError("备用金申请创建失败")
  93. @staticmethod
  94. def update(
  95. db: Session,
  96. advance_id: str,
  97. obj_in: dict,
  98. *,
  99. user: BxUserContext,
  100. ) -> Optional[BxAdvanceApply]:
  101. tenant_no = AdvanceService._tenant_no(db, user)
  102. return BxCrudService.update(
  103. BxAdvanceApply,
  104. db,
  105. advance_id,
  106. tenant_no,
  107. obj_in,
  108. user_id=user.id,
  109. user=user,
  110. )
  111. @staticmethod
  112. def soft_delete(db: Session, advance_id: str, *, user: BxUserContext) -> bool:
  113. tenant_no = AdvanceService._tenant_no(db, user)
  114. return BxCrudService.soft_delete(
  115. BxAdvanceApply, db, advance_id, tenant_no, user_id=user.id, user=user
  116. )
  117. @staticmethod
  118. def list_scope_filter(db: Session, user: BxUserContext, tenant_no: str, scope: str):
  119. """列头 facet 与列表视图对齐。
  120. - pending / approved:审批待办 / 我已审批
  121. - mine:我的申请(申请人或创建人)
  122. - issue_pending / issue_issued:备用金待发放 / 已发放
  123. """
  124. key = (scope or "").strip()
  125. if key == "pending":
  126. return BxAdvanceApply.id.in_(
  127. db.query(BxAdvanceApply.id)
  128. .join(
  129. BxWorkflowNode,
  130. and_(
  131. BxWorkflowNode.wf_id == BxAdvanceApply.wf_id,
  132. BxWorkflowNode.node_name == BxAdvanceApply.current_node,
  133. BxWorkflowNode.tenant_no == tenant_no,
  134. BxWorkflowNode.del_flag == 0,
  135. ),
  136. )
  137. .filter(
  138. BxAdvanceApply.del_flag == 0,
  139. BxAdvanceApply.tenant_no == tenant_no,
  140. BxAdvanceApply.status == "pending",
  141. BxWorkflowNode.approver_id == user.id,
  142. )
  143. )
  144. if key == "approved":
  145. return BxAdvanceApply.id.in_(
  146. db.query(BxAdvanceApproval.apply_id)
  147. .filter(
  148. BxAdvanceApproval.approver_id == user.id,
  149. BxAdvanceApproval.tenant_no == tenant_no,
  150. BxAdvanceApproval.del_flag == 0,
  151. )
  152. .distinct()
  153. )
  154. if key == "mine":
  155. return or_(
  156. BxAdvanceApply.applicant_id == user.id,
  157. BxAdvanceApply.create_by == user.id,
  158. )
  159. if key == "issue_pending":
  160. return and_(
  161. BxAdvanceApply.status == "approved",
  162. or_(
  163. BxAdvanceApply.issue_status.is_(None),
  164. BxAdvanceApply.issue_status != "issued",
  165. ),
  166. )
  167. if key == "issue_issued":
  168. return BxAdvanceApply.status == "issued"
  169. raise ValueError(
  170. "不支持的 scope,仅支持 pending | approved | mine | issue_pending | issue_issued"
  171. )
  172. # 兼容旧名
  173. approval_list_scope_filter = list_scope_filter
  174. @staticmethod
  175. def pending_approval(
  176. db: Session,
  177. user: BxUserContext,
  178. tenant_no: str,
  179. page: int,
  180. page_size: int,
  181. keyword: str = "",
  182. applicant_id: Optional[str] = None,
  183. apply_date: Optional[str] = None,
  184. apply_date_from: Optional[str] = None,
  185. apply_date_to: Optional[str] = None,
  186. extra_filters: Optional[list] = None,
  187. order_by: Optional[Any] = None,
  188. ):
  189. subquery = (
  190. db.query(BxAdvanceApply.id)
  191. .join(
  192. BxWorkflowNode,
  193. and_(
  194. BxWorkflowNode.wf_id == BxAdvanceApply.wf_id,
  195. BxWorkflowNode.node_name == BxAdvanceApply.current_node,
  196. BxWorkflowNode.tenant_no == tenant_no,
  197. BxWorkflowNode.del_flag == 0,
  198. ),
  199. )
  200. .filter(
  201. BxAdvanceApply.del_flag == 0,
  202. BxAdvanceApply.tenant_no == tenant_no,
  203. BxAdvanceApply.status == "pending",
  204. BxWorkflowNode.approver_id == user.id,
  205. )
  206. )
  207. if extra_filters:
  208. subquery = subquery.filter(*extra_filters)
  209. else:
  210. if keyword:
  211. subquery = subquery.filter(
  212. or_(
  213. BxAdvanceApply.apply_code.contains(keyword),
  214. BxAdvanceApply.applicant_name.contains(keyword),
  215. )
  216. )
  217. if applicant_id:
  218. subquery = subquery.filter(BxAdvanceApply.applicant_id == applicant_id)
  219. start, end = resolve_create_time_bounds(
  220. apply_date=apply_date,
  221. apply_date_from=apply_date_from,
  222. apply_date_to=apply_date_to,
  223. )
  224. if start is not None:
  225. subquery = subquery.filter(BxAdvanceApply.create_time >= start)
  226. if end is not None:
  227. subquery = subquery.filter(BxAdvanceApply.create_time < end)
  228. subquery = subquery.subquery()
  229. total = db.query(func.count()).select_from(subquery).scalar() or 0
  230. order_expr = order_by if order_by is not None else BxAdvanceApply.create_time.desc()
  231. rows = (
  232. db.query(BxAdvanceApply)
  233. .join(subquery, BxAdvanceApply.id == subquery.c.id)
  234. .order_by(order_expr)
  235. .offset((page - 1) * page_size)
  236. .limit(page_size)
  237. .all()
  238. )
  239. return rows, total
  240. @staticmethod
  241. def approved_by_me(
  242. db: Session,
  243. user: BxUserContext,
  244. tenant_no: str,
  245. page: int,
  246. page_size: int,
  247. keyword: str = "",
  248. applicant_id: Optional[str] = None,
  249. apply_date: Optional[str] = None,
  250. apply_date_from: Optional[str] = None,
  251. apply_date_to: Optional[str] = None,
  252. extra_filters: Optional[list] = None,
  253. order_by: Optional[Any] = None,
  254. ):
  255. subquery = (
  256. db.query(BxAdvanceApproval.apply_id)
  257. .filter(
  258. BxAdvanceApproval.approver_id == user.id,
  259. BxAdvanceApproval.tenant_no == tenant_no,
  260. BxAdvanceApproval.del_flag == 0,
  261. )
  262. .distinct()
  263. .subquery()
  264. )
  265. query = (
  266. db.query(BxAdvanceApply)
  267. .join(subquery, BxAdvanceApply.id == subquery.c.apply_id)
  268. .filter(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no)
  269. )
  270. if extra_filters:
  271. query = query.filter(*extra_filters)
  272. else:
  273. if keyword:
  274. query = query.filter(
  275. or_(
  276. BxAdvanceApply.apply_code.contains(keyword),
  277. BxAdvanceApply.applicant_name.contains(keyword),
  278. )
  279. )
  280. if applicant_id:
  281. query = query.filter(BxAdvanceApply.applicant_id == applicant_id)
  282. start, end = resolve_create_time_bounds(
  283. apply_date=apply_date,
  284. apply_date_from=apply_date_from,
  285. apply_date_to=apply_date_to,
  286. )
  287. if start is not None:
  288. query = query.filter(BxAdvanceApply.create_time >= start)
  289. if end is not None:
  290. query = query.filter(BxAdvanceApply.create_time < end)
  291. query = apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
  292. total = query.count()
  293. order_expr = order_by if order_by is not None else BxAdvanceApply.create_time.desc()
  294. rows = (
  295. query.order_by(order_expr)
  296. .offset((page - 1) * page_size)
  297. .limit(page_size)
  298. .all()
  299. )
  300. return rows, total
  301. @staticmethod
  302. def _workflow_nodes(db: Session, wf_id: str, tenant_no: str) -> List[BxWorkflowNode]:
  303. return (
  304. db.query(BxWorkflowNode)
  305. .filter(
  306. BxWorkflowNode.wf_id == wf_id,
  307. BxWorkflowNode.tenant_no == tenant_no,
  308. BxWorkflowNode.del_flag == 0,
  309. )
  310. .order_by(BxWorkflowNode.node_order)
  311. .all()
  312. )
  313. @staticmethod
  314. def submit(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxAdvanceApply]:
  315. tenant_no = AdvanceService._tenant_no(db, user)
  316. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
  317. if not apply_obj:
  318. return None
  319. apply_obj.status = "pending"
  320. if not apply_obj.apply_code:
  321. apply_obj.apply_code = generate_apply_code(db, "BYJ", tenant_no)
  322. apply_obj.update_by = user.id
  323. if apply_obj.wf_id:
  324. nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
  325. if nodes:
  326. apply_obj.current_node = nodes[0].node_name
  327. db.flush()
  328. return apply_obj
  329. @staticmethod
  330. def approve(
  331. db: Session, apply_id: str, approver: BxUserContext, opinion: str
  332. ) -> Optional[BxAdvanceApply]:
  333. tenant_no = AdvanceService._tenant_no(db, approver)
  334. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, skip_scope=True)
  335. if not apply_obj or not apply_obj.wf_id:
  336. return None
  337. nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
  338. if not nodes:
  339. return None
  340. current_idx = None
  341. for i, node in enumerate(nodes):
  342. if node.node_name == apply_obj.current_node:
  343. current_idx = i
  344. break
  345. if current_idx is None:
  346. return None
  347. current_node = nodes[current_idx]
  348. if current_node.approver_id != approver.id:
  349. return None
  350. approval = BxAdvanceApproval(
  351. id=generate_uuid(),
  352. apply_id=apply_id,
  353. approver_id=approver.id,
  354. approver_name=approver.name,
  355. approval_opinion=opinion,
  356. node_name=current_node.node_name,
  357. node_order=current_node.node_order,
  358. approval_status="approved",
  359. approval_time=datetime.now(),
  360. tenant_no=tenant_no,
  361. create_by=approver.id,
  362. create_dept_id=approver.dept_id or "",
  363. )
  364. db.add(approval)
  365. if current_idx == len(nodes) - 1:
  366. apply_obj.status = "approved"
  367. apply_obj.current_node = None
  368. else:
  369. apply_obj.current_node = nodes[current_idx + 1].node_name
  370. apply_obj.update_by = approver.id
  371. db.flush()
  372. return apply_obj
  373. @staticmethod
  374. def reject(
  375. db: Session, apply_id: str, approver: BxUserContext, opinion: str
  376. ) -> Optional[BxAdvanceApply]:
  377. tenant_no = AdvanceService._tenant_no(db, approver)
  378. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, skip_scope=True)
  379. if not apply_obj or not apply_obj.wf_id:
  380. return None
  381. nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
  382. current_node = None
  383. for node in nodes:
  384. if node.node_name == apply_obj.current_node:
  385. current_node = node
  386. break
  387. if current_node is None or current_node.approver_id != approver.id:
  388. return None
  389. approval = BxAdvanceApproval(
  390. id=generate_uuid(),
  391. apply_id=apply_id,
  392. approver_id=approver.id,
  393. approver_name=approver.name,
  394. approval_opinion=opinion,
  395. node_name=current_node.node_name,
  396. node_order=current_node.node_order,
  397. approval_status="rejected",
  398. approval_time=datetime.now(),
  399. tenant_no=tenant_no,
  400. create_by=approver.id,
  401. create_dept_id=approver.dept_id or "",
  402. )
  403. db.add(approval)
  404. apply_obj.status = "rejected"
  405. apply_obj.current_node = None
  406. apply_obj.update_by = approver.id
  407. db.flush()
  408. return apply_obj
  409. @staticmethod
  410. def withdraw(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxAdvanceApply]:
  411. tenant_no = AdvanceService._tenant_no(db, user)
  412. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
  413. if not apply_obj:
  414. return None
  415. if apply_obj.applicant_id != user.id:
  416. return None
  417. if apply_obj.status != "pending":
  418. return None
  419. apply_obj.status = "draft"
  420. apply_obj.current_node = None
  421. apply_obj.update_by = user.id
  422. db.flush()
  423. return apply_obj
  424. @staticmethod
  425. def _approval_record_dict(approval: BxAdvanceApproval, apply_id: str) -> Dict[str, Any]:
  426. return {
  427. "id": approval.id,
  428. "advance_id": apply_id,
  429. "node_name": approval.node_name,
  430. "node_order": approval.node_order,
  431. "approval_status": approval.approval_status,
  432. "approval_opinion": approval.approval_opinion,
  433. "approver_name": approval.approver_name,
  434. "approval_time": approval.approval_time,
  435. }
  436. @staticmethod
  437. def _latest_approval_per_node(
  438. approvals: Sequence[BxAdvanceApproval],
  439. ) -> Dict[int, BxAdvanceApproval]:
  440. """同一节点多次审批时,取 approval_time 最新的一条作为当前节点状态。"""
  441. approval_map: Dict[int, BxAdvanceApproval] = {}
  442. for approval in approvals:
  443. existing = approval_map.get(approval.node_order)
  444. if existing is None:
  445. approval_map[approval.node_order] = approval
  446. continue
  447. approval_time = approval.approval_time or datetime.min
  448. existing_time = existing.approval_time or datetime.min
  449. if approval_time >= existing_time:
  450. approval_map[approval.node_order] = approval
  451. return approval_map
  452. @staticmethod
  453. def _query_approvals(db: Session, apply_id: str, tenant_no: str) -> List[BxAdvanceApproval]:
  454. return (
  455. db.query(BxAdvanceApproval)
  456. .filter(
  457. BxAdvanceApproval.apply_id == apply_id,
  458. BxAdvanceApproval.tenant_no == tenant_no,
  459. BxAdvanceApproval.del_flag == 0,
  460. )
  461. .all()
  462. )
  463. @staticmethod
  464. def get_approvals(
  465. db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
  466. ) -> List[Dict[str, Any]]:
  467. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
  468. if not apply_obj or not apply_obj.wf_id:
  469. return []
  470. wf = BxCrudService.get_by_id(BxWorkflowDef, db, apply_obj.wf_id, tenant_no)
  471. if not wf:
  472. return []
  473. nodes = AdvanceService._workflow_nodes(db, wf.id, tenant_no)
  474. if not nodes:
  475. return []
  476. approvals = AdvanceService._query_approvals(db, apply_id, tenant_no)
  477. cycle_approvals = filter_current_cycle_approvals(approvals, apply_obj.status)
  478. approval_map = AdvanceService._latest_approval_per_node(cycle_approvals)
  479. result: List[Dict[str, Any]] = []
  480. for node in nodes:
  481. approval = approval_map.get(node.node_order)
  482. if approval:
  483. result.append(AdvanceService._approval_record_dict(approval, apply_id))
  484. else:
  485. result.append(
  486. {
  487. "id": "0",
  488. "advance_id": apply_id,
  489. "node_name": node.node_name,
  490. "node_order": node.node_order,
  491. "approval_status": "pending",
  492. "approval_opinion": None,
  493. "approver_name": node.approver_name or "",
  494. "approval_time": None,
  495. }
  496. )
  497. return result
  498. @staticmethod
  499. def get_approval_history(
  500. db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
  501. ) -> List[Dict[str, Any]]:
  502. """返回全部审批动作,按时间正序排列,用于展示完整审批历史。"""
  503. if not AdvanceService.get_by_id(db, apply_id, tenant_no, user=user):
  504. return []
  505. approvals = (
  506. db.query(BxAdvanceApproval)
  507. .filter(
  508. BxAdvanceApproval.apply_id == apply_id,
  509. BxAdvanceApproval.tenant_no == tenant_no,
  510. BxAdvanceApproval.del_flag == 0,
  511. )
  512. .order_by(BxAdvanceApproval.approval_time.asc(), BxAdvanceApproval.create_time.asc())
  513. .all()
  514. )
  515. return [AdvanceService._approval_record_dict(a, apply_id) for a in approvals]
  516. @staticmethod
  517. def global_status_counts(db: Session, tenant_no: str, *, user: BxUserContext) -> Dict[str, int]:
  518. query = db.query(BxAdvanceApply.status, func.count()).filter(
  519. BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no
  520. )
  521. query = apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
  522. rows = query.group_by(BxAdvanceApply.status).all()
  523. return {str(s or ""): int(c) for s, c in rows}
  524. @staticmethod
  525. def _my_apply_base(db: Session, user: BxUserContext, tenant_no: str):
  526. query = db.query(BxAdvanceApply).filter(
  527. BxAdvanceApply.del_flag == 0,
  528. BxAdvanceApply.tenant_no == tenant_no,
  529. or_(
  530. BxAdvanceApply.applicant_id == user.id,
  531. BxAdvanceApply.create_by == user.id,
  532. ),
  533. )
  534. return apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
  535. @staticmethod
  536. def my_apply_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict:
  537. base_q = AdvanceService._my_apply_base(db, user, tenant_no).subquery()
  538. base = BxAdvanceApply.id.in_(db.query(base_q.c.id))
  539. total_count = (
  540. db.query(func.count()).select_from(BxAdvanceApply).filter(base).scalar() or 0
  541. )
  542. pending = (
  543. db.query(func.count())
  544. .select_from(BxAdvanceApply)
  545. .filter(base, BxAdvanceApply.status == "pending")
  546. .scalar()
  547. or 0
  548. )
  549. approved_pass = (
  550. db.query(func.count())
  551. .select_from(BxAdvanceApply)
  552. .filter(base, BxAdvanceApply.status == "approved")
  553. .scalar()
  554. or 0
  555. )
  556. rejected_count = (
  557. db.query(func.count())
  558. .select_from(BxAdvanceApply)
  559. .filter(base, BxAdvanceApply.status == "rejected")
  560. .scalar()
  561. or 0
  562. )
  563. pending_issue = approved_pass
  564. issued = (
  565. db.query(func.count())
  566. .select_from(BxAdvanceApply)
  567. .filter(base, BxAdvanceApply.status == "issued")
  568. .scalar()
  569. or 0
  570. )
  571. issued_amount = (
  572. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  573. .filter(base, BxAdvanceApply.status == "issued")
  574. .scalar()
  575. or 0
  576. )
  577. m0, m1 = current_month_bounds()
  578. month_apply_amount = (
  579. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  580. .filter(base, BxAdvanceApply.create_time >= m0, BxAdvanceApply.create_time < m1)
  581. .scalar()
  582. or 0
  583. )
  584. month_apply_count = (
  585. db.query(func.count())
  586. .select_from(BxAdvanceApply)
  587. .filter(base, BxAdvanceApply.create_time >= m0, BxAdvanceApply.create_time < m1)
  588. .scalar()
  589. or 0
  590. )
  591. offset_total = (
  592. db.query(func.coalesce(func.sum(BxReimburseOffset.offset_amount), 0))
  593. .join(BxAdvanceApply, BxReimburseOffset.advance_id == BxAdvanceApply.id)
  594. .filter(
  595. BxAdvanceApply.id.in_(db.query(base_q.c.id)),
  596. BxReimburseOffset.tenant_no == tenant_no,
  597. BxReimburseOffset.del_flag == 0,
  598. )
  599. .scalar()
  600. or 0
  601. )
  602. return {
  603. "total_count": int(total_count),
  604. "pending_count": int(pending),
  605. "approved_pass_count": int(approved_pass),
  606. "rejected_count": int(rejected_count),
  607. "pending_issue_count": int(pending_issue),
  608. "issued_count": int(issued),
  609. "issued_amount": float(issued_amount),
  610. "month_apply_amount": float(month_apply_amount),
  611. "month_apply_count": int(month_apply_count),
  612. "offset_total": float(offset_total),
  613. }
  614. @staticmethod
  615. def pending_approval_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict:
  616. subquery = (
  617. db.query(BxAdvanceApply.id)
  618. .join(
  619. BxWorkflowNode,
  620. and_(
  621. BxWorkflowNode.wf_id == BxAdvanceApply.wf_id,
  622. BxWorkflowNode.node_name == BxAdvanceApply.current_node,
  623. BxWorkflowNode.tenant_no == tenant_no,
  624. BxWorkflowNode.del_flag == 0,
  625. ),
  626. )
  627. .filter(
  628. BxAdvanceApply.del_flag == 0,
  629. BxAdvanceApply.tenant_no == tenant_no,
  630. BxAdvanceApply.status == "pending",
  631. BxWorkflowNode.approver_id == user.id,
  632. )
  633. ).subquery()
  634. total = db.query(func.count()).select_from(subquery).scalar() or 0
  635. m0, m1 = current_month_bounds()
  636. month_sum = (
  637. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  638. .join(subquery, BxAdvanceApply.id == subquery.c.id)
  639. .filter(BxAdvanceApply.create_time >= m0, BxAdvanceApply.create_time < m1)
  640. .scalar()
  641. or 0
  642. )
  643. return {"pending_total": int(total), "month_pending_amount": float(month_sum)}
  644. @staticmethod
  645. def offset_amount_map(db: Session, tenant_no: str, advance_ids: List[str]) -> Dict[str, float]:
  646. if not advance_ids:
  647. return {}
  648. rows = (
  649. db.query(
  650. BxReimburseOffset.advance_id,
  651. func.coalesce(func.sum(BxReimburseOffset.offset_amount), 0),
  652. )
  653. .filter(
  654. BxReimburseOffset.advance_id.in_(advance_ids),
  655. BxReimburseOffset.tenant_no == tenant_no,
  656. BxReimburseOffset.del_flag == 0,
  657. )
  658. .group_by(BxReimburseOffset.advance_id)
  659. .all()
  660. )
  661. return {str(advance_id): float(amount) for advance_id, amount in rows}
  662. @staticmethod
  663. def issue(
  664. db: Session,
  665. advance_id: str,
  666. user: BxUserContext,
  667. remark: str = "",
  668. actual_amount: Optional[float] = None,
  669. ) -> Optional[BxAdvanceApply]:
  670. tenant_no = AdvanceService._tenant_no(db, user)
  671. apply_obj = AdvanceService.get_by_id(db, advance_id, tenant_no, user=user)
  672. if not apply_obj:
  673. return None
  674. apply_obj.status = "issued"
  675. apply_obj.update_by = user.id
  676. apply_obj.issue_status = "issued"
  677. apply_obj.issue_time = datetime.now()
  678. apply_obj.issue_user_id = user.id
  679. apply_obj.issue_user_name = user.name
  680. if remark:
  681. apply_obj.issue_remark = remark
  682. if actual_amount is not None:
  683. apply_obj.actual_amount = actual_amount
  684. db.flush()
  685. return apply_obj
  686. @staticmethod
  687. def finance_issue_stats(db: Session, tenant_no: str, *, user: BxUserContext) -> dict:
  688. base_q = (
  689. db.query(BxAdvanceApply)
  690. .filter(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no)
  691. )
  692. base_q = apply_bx_scope(base_q, BxAdvanceApply, user, db, tenant_no).subquery()
  693. base = BxAdvanceApply.id.in_(db.query(base_q.c.id))
  694. pending_f = and_(
  695. BxAdvanceApply.status == "approved",
  696. or_(BxAdvanceApply.issue_status.is_(None), BxAdvanceApply.issue_status != "issued"),
  697. )
  698. pending_n = db.query(func.count()).select_from(BxAdvanceApply).filter(base, pending_f).scalar() or 0
  699. issued_n = (
  700. db.query(func.count())
  701. .select_from(BxAdvanceApply)
  702. .filter(base, BxAdvanceApply.status == "issued")
  703. .scalar()
  704. or 0
  705. )
  706. pending_amount = (
  707. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  708. .filter(base, pending_f)
  709. .scalar()
  710. or 0
  711. )
  712. m0, m1 = current_month_bounds()
  713. amt_expr = func.coalesce(BxAdvanceApply.actual_amount, BxAdvanceApply.amount)
  714. issued_month = (
  715. db.query(func.coalesce(func.sum(amt_expr), 0))
  716. .filter(
  717. base,
  718. BxAdvanceApply.status == "issued",
  719. BxAdvanceApply.issue_time.isnot(None),
  720. BxAdvanceApply.issue_time >= m0,
  721. BxAdvanceApply.issue_time < m1,
  722. )
  723. .scalar()
  724. or 0
  725. )
  726. return {
  727. "pending_count": int(pending_n),
  728. "issued_count": int(issued_n),
  729. "pending_amount": float(pending_amount),
  730. "issued_month_amount": float(issued_month),
  731. }