advance.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  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. ) -> Tuple[Sequence[BxAdvanceApply], int]:
  47. return BxCrudService.get_paged(
  48. BxAdvanceApply, db, tenant_no, page, page_size, filters, user=user
  49. )
  50. @staticmethod
  51. def create(
  52. db: Session,
  53. obj_in: dict,
  54. *,
  55. user: BxUserContext,
  56. ) -> BxAdvanceApply:
  57. tenant_no = AdvanceService._tenant_no(db, user)
  58. return BxCrudService.create(
  59. BxAdvanceApply,
  60. db,
  61. obj_in,
  62. tenant_no=tenant_no,
  63. user_id=user.id,
  64. dept_id=user.dept_id,
  65. )
  66. @staticmethod
  67. def update(
  68. db: Session,
  69. advance_id: str,
  70. obj_in: dict,
  71. *,
  72. user: BxUserContext,
  73. ) -> Optional[BxAdvanceApply]:
  74. tenant_no = AdvanceService._tenant_no(db, user)
  75. return BxCrudService.update(
  76. BxAdvanceApply,
  77. db,
  78. advance_id,
  79. tenant_no,
  80. obj_in,
  81. user_id=user.id,
  82. user=user,
  83. )
  84. @staticmethod
  85. def soft_delete(db: Session, advance_id: str, *, user: BxUserContext) -> bool:
  86. tenant_no = AdvanceService._tenant_no(db, user)
  87. return BxCrudService.soft_delete(
  88. BxAdvanceApply, db, advance_id, tenant_no, user_id=user.id, user=user
  89. )
  90. @staticmethod
  91. def pending_approval(
  92. db: Session,
  93. user: BxUserContext,
  94. tenant_no: str,
  95. page: int,
  96. page_size: int,
  97. keyword: str = "",
  98. applicant_id: Optional[str] = None,
  99. apply_date: Optional[str] = None,
  100. apply_date_from: Optional[str] = None,
  101. apply_date_to: Optional[str] = None,
  102. ):
  103. subquery = (
  104. db.query(BxAdvanceApply.id)
  105. .join(
  106. BxWorkflowNode,
  107. and_(
  108. BxWorkflowNode.wf_id == BxAdvanceApply.wf_id,
  109. BxWorkflowNode.node_name == BxAdvanceApply.current_node,
  110. BxWorkflowNode.tenant_no == tenant_no,
  111. BxWorkflowNode.del_flag == 0,
  112. ),
  113. )
  114. .filter(
  115. BxAdvanceApply.del_flag == 0,
  116. BxAdvanceApply.tenant_no == tenant_no,
  117. BxAdvanceApply.status == "pending",
  118. BxWorkflowNode.approver_id == user.id,
  119. )
  120. )
  121. if keyword:
  122. subquery = subquery.filter(
  123. or_(
  124. BxAdvanceApply.apply_code.contains(keyword),
  125. BxAdvanceApply.applicant_name.contains(keyword),
  126. )
  127. )
  128. if applicant_id:
  129. subquery = subquery.filter(BxAdvanceApply.applicant_id == applicant_id)
  130. start, end = resolve_create_time_bounds(
  131. apply_date=apply_date,
  132. apply_date_from=apply_date_from,
  133. apply_date_to=apply_date_to,
  134. )
  135. if start is not None:
  136. subquery = subquery.filter(BxAdvanceApply.create_time >= start)
  137. if end is not None:
  138. subquery = subquery.filter(BxAdvanceApply.create_time < end)
  139. subquery = subquery.subquery()
  140. total = db.query(func.count()).select_from(subquery).scalar() or 0
  141. rows = (
  142. db.query(BxAdvanceApply)
  143. .join(subquery, BxAdvanceApply.id == subquery.c.id)
  144. .order_by(BxAdvanceApply.create_time.desc())
  145. .offset((page - 1) * page_size)
  146. .limit(page_size)
  147. .all()
  148. )
  149. return rows, total
  150. @staticmethod
  151. def approved_by_me(
  152. db: Session,
  153. user: BxUserContext,
  154. tenant_no: str,
  155. page: int,
  156. page_size: int,
  157. keyword: str = "",
  158. applicant_id: Optional[str] = None,
  159. apply_date: Optional[str] = None,
  160. apply_date_from: Optional[str] = None,
  161. apply_date_to: Optional[str] = None,
  162. ):
  163. subquery = (
  164. db.query(BxAdvanceApproval.apply_id)
  165. .filter(
  166. BxAdvanceApproval.approver_id == user.id,
  167. BxAdvanceApproval.tenant_no == tenant_no,
  168. BxAdvanceApproval.del_flag == 0,
  169. )
  170. .distinct()
  171. .subquery()
  172. )
  173. query = (
  174. db.query(BxAdvanceApply)
  175. .join(subquery, BxAdvanceApply.id == subquery.c.apply_id)
  176. .filter(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no)
  177. )
  178. if keyword:
  179. query = query.filter(
  180. or_(
  181. BxAdvanceApply.apply_code.contains(keyword),
  182. BxAdvanceApply.applicant_name.contains(keyword),
  183. )
  184. )
  185. if applicant_id:
  186. query = query.filter(BxAdvanceApply.applicant_id == applicant_id)
  187. start, end = resolve_create_time_bounds(
  188. apply_date=apply_date,
  189. apply_date_from=apply_date_from,
  190. apply_date_to=apply_date_to,
  191. )
  192. if start is not None:
  193. query = query.filter(BxAdvanceApply.create_time >= start)
  194. if end is not None:
  195. query = query.filter(BxAdvanceApply.create_time < end)
  196. query = apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
  197. total = query.count()
  198. rows = (
  199. query.order_by(BxAdvanceApply.create_time.desc())
  200. .offset((page - 1) * page_size)
  201. .limit(page_size)
  202. .all()
  203. )
  204. return rows, total
  205. @staticmethod
  206. def _workflow_nodes(db: Session, wf_id: str, tenant_no: str) -> List[BxWorkflowNode]:
  207. return (
  208. db.query(BxWorkflowNode)
  209. .filter(
  210. BxWorkflowNode.wf_id == wf_id,
  211. BxWorkflowNode.tenant_no == tenant_no,
  212. BxWorkflowNode.del_flag == 0,
  213. )
  214. .order_by(BxWorkflowNode.node_order)
  215. .all()
  216. )
  217. @staticmethod
  218. def submit(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxAdvanceApply]:
  219. tenant_no = AdvanceService._tenant_no(db, user)
  220. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
  221. if not apply_obj:
  222. return None
  223. apply_obj.status = "pending"
  224. if not apply_obj.apply_code:
  225. apply_obj.apply_code = generate_apply_code(db, "BYJ", tenant_no)
  226. apply_obj.update_by = user.id
  227. if apply_obj.wf_id:
  228. nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
  229. if nodes:
  230. apply_obj.current_node = nodes[0].node_name
  231. db.flush()
  232. return apply_obj
  233. @staticmethod
  234. def approve(
  235. db: Session, apply_id: str, approver: BxUserContext, opinion: str
  236. ) -> Optional[BxAdvanceApply]:
  237. tenant_no = AdvanceService._tenant_no(db, approver)
  238. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, skip_scope=True)
  239. if not apply_obj or not apply_obj.wf_id:
  240. return None
  241. nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
  242. if not nodes:
  243. return None
  244. current_idx = None
  245. for i, node in enumerate(nodes):
  246. if node.node_name == apply_obj.current_node:
  247. current_idx = i
  248. break
  249. if current_idx is None:
  250. return None
  251. current_node = nodes[current_idx]
  252. if current_node.approver_id != approver.id:
  253. return None
  254. approval = BxAdvanceApproval(
  255. id=generate_uuid(),
  256. apply_id=apply_id,
  257. approver_id=approver.id,
  258. approver_name=approver.name,
  259. approval_opinion=opinion,
  260. node_name=current_node.node_name,
  261. node_order=current_node.node_order,
  262. approval_status="approved",
  263. approval_time=datetime.now(),
  264. tenant_no=tenant_no,
  265. create_by=approver.id,
  266. create_dept_id=approver.dept_id or "",
  267. )
  268. db.add(approval)
  269. if current_idx == len(nodes) - 1:
  270. apply_obj.status = "approved"
  271. apply_obj.current_node = None
  272. else:
  273. apply_obj.current_node = nodes[current_idx + 1].node_name
  274. apply_obj.update_by = approver.id
  275. db.flush()
  276. return apply_obj
  277. @staticmethod
  278. def reject(
  279. db: Session, apply_id: str, approver: BxUserContext, opinion: str
  280. ) -> Optional[BxAdvanceApply]:
  281. tenant_no = AdvanceService._tenant_no(db, approver)
  282. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, skip_scope=True)
  283. if not apply_obj or not apply_obj.wf_id:
  284. return None
  285. nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
  286. current_node = None
  287. for node in nodes:
  288. if node.node_name == apply_obj.current_node:
  289. current_node = node
  290. break
  291. if current_node is None or current_node.approver_id != approver.id:
  292. return None
  293. approval = BxAdvanceApproval(
  294. id=generate_uuid(),
  295. apply_id=apply_id,
  296. approver_id=approver.id,
  297. approver_name=approver.name,
  298. approval_opinion=opinion,
  299. node_name=current_node.node_name,
  300. node_order=current_node.node_order,
  301. approval_status="rejected",
  302. approval_time=datetime.now(),
  303. tenant_no=tenant_no,
  304. create_by=approver.id,
  305. create_dept_id=approver.dept_id or "",
  306. )
  307. db.add(approval)
  308. apply_obj.status = "rejected"
  309. apply_obj.current_node = None
  310. apply_obj.update_by = approver.id
  311. db.flush()
  312. return apply_obj
  313. @staticmethod
  314. def _approval_record_dict(approval: BxAdvanceApproval, apply_id: str) -> Dict[str, Any]:
  315. return {
  316. "id": approval.id,
  317. "advance_id": apply_id,
  318. "node_name": approval.node_name,
  319. "node_order": approval.node_order,
  320. "approval_status": approval.approval_status,
  321. "approval_opinion": approval.approval_opinion,
  322. "approver_name": approval.approver_name,
  323. "approval_time": approval.approval_time,
  324. }
  325. @staticmethod
  326. def _latest_approval_per_node(
  327. approvals: Sequence[BxAdvanceApproval],
  328. ) -> Dict[int, BxAdvanceApproval]:
  329. """同一节点多次审批时,取 approval_time 最新的一条作为当前节点状态。"""
  330. approval_map: Dict[int, BxAdvanceApproval] = {}
  331. for approval in approvals:
  332. existing = approval_map.get(approval.node_order)
  333. if existing is None:
  334. approval_map[approval.node_order] = approval
  335. continue
  336. approval_time = approval.approval_time or datetime.min
  337. existing_time = existing.approval_time or datetime.min
  338. if approval_time >= existing_time:
  339. approval_map[approval.node_order] = approval
  340. return approval_map
  341. @staticmethod
  342. def _query_approvals(db: Session, apply_id: str, tenant_no: str) -> List[BxAdvanceApproval]:
  343. return (
  344. db.query(BxAdvanceApproval)
  345. .filter(
  346. BxAdvanceApproval.apply_id == apply_id,
  347. BxAdvanceApproval.tenant_no == tenant_no,
  348. BxAdvanceApproval.del_flag == 0,
  349. )
  350. .all()
  351. )
  352. @staticmethod
  353. def get_approvals(
  354. db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
  355. ) -> List[Dict[str, Any]]:
  356. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
  357. if not apply_obj or not apply_obj.wf_id:
  358. return []
  359. wf = BxCrudService.get_by_id(BxWorkflowDef, db, apply_obj.wf_id, tenant_no)
  360. if not wf:
  361. return []
  362. nodes = AdvanceService._workflow_nodes(db, wf.id, tenant_no)
  363. if not nodes:
  364. return []
  365. approvals = AdvanceService._query_approvals(db, apply_id, tenant_no)
  366. cycle_approvals = filter_current_cycle_approvals(approvals, apply_obj.status)
  367. approval_map = AdvanceService._latest_approval_per_node(cycle_approvals)
  368. result: List[Dict[str, Any]] = []
  369. for node in nodes:
  370. approval = approval_map.get(node.node_order)
  371. if approval:
  372. result.append(AdvanceService._approval_record_dict(approval, apply_id))
  373. else:
  374. result.append(
  375. {
  376. "id": "0",
  377. "advance_id": apply_id,
  378. "node_name": node.node_name,
  379. "node_order": node.node_order,
  380. "approval_status": "pending",
  381. "approval_opinion": None,
  382. "approver_name": node.approver_name or "",
  383. "approval_time": None,
  384. }
  385. )
  386. return result
  387. @staticmethod
  388. def get_approval_history(
  389. db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
  390. ) -> List[Dict[str, Any]]:
  391. """返回全部审批动作,按时间正序排列,用于展示完整审批历史。"""
  392. if not AdvanceService.get_by_id(db, apply_id, tenant_no, user=user):
  393. return []
  394. approvals = (
  395. db.query(BxAdvanceApproval)
  396. .filter(
  397. BxAdvanceApproval.apply_id == apply_id,
  398. BxAdvanceApproval.tenant_no == tenant_no,
  399. BxAdvanceApproval.del_flag == 0,
  400. )
  401. .order_by(BxAdvanceApproval.approval_time.asc(), BxAdvanceApproval.create_time.asc())
  402. .all()
  403. )
  404. return [AdvanceService._approval_record_dict(a, apply_id) for a in approvals]
  405. @staticmethod
  406. def global_status_counts(db: Session, tenant_no: str, *, user: BxUserContext) -> Dict[str, int]:
  407. query = db.query(BxAdvanceApply.status, func.count()).filter(
  408. BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no
  409. )
  410. query = apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
  411. rows = query.group_by(BxAdvanceApply.status).all()
  412. return {str(s or ""): int(c) for s, c in rows}
  413. @staticmethod
  414. def _my_apply_base(db: Session, user: BxUserContext, tenant_no: str):
  415. query = db.query(BxAdvanceApply).filter(
  416. BxAdvanceApply.del_flag == 0,
  417. BxAdvanceApply.tenant_no == tenant_no,
  418. or_(
  419. BxAdvanceApply.applicant_id == user.id,
  420. BxAdvanceApply.create_by == user.id,
  421. ),
  422. )
  423. return apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
  424. @staticmethod
  425. def my_apply_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict:
  426. base_q = AdvanceService._my_apply_base(db, user, tenant_no).subquery()
  427. base = BxAdvanceApply.id.in_(db.query(base_q.c.id))
  428. total_count = (
  429. db.query(func.count()).select_from(BxAdvanceApply).filter(base).scalar() or 0
  430. )
  431. pending = (
  432. db.query(func.count())
  433. .select_from(BxAdvanceApply)
  434. .filter(base, BxAdvanceApply.status == "pending")
  435. .scalar()
  436. or 0
  437. )
  438. approved_pass = (
  439. db.query(func.count())
  440. .select_from(BxAdvanceApply)
  441. .filter(base, BxAdvanceApply.status == "approved")
  442. .scalar()
  443. or 0
  444. )
  445. rejected_count = (
  446. db.query(func.count())
  447. .select_from(BxAdvanceApply)
  448. .filter(base, BxAdvanceApply.status == "rejected")
  449. .scalar()
  450. or 0
  451. )
  452. pending_issue = approved_pass
  453. issued = (
  454. db.query(func.count())
  455. .select_from(BxAdvanceApply)
  456. .filter(base, BxAdvanceApply.status == "issued")
  457. .scalar()
  458. or 0
  459. )
  460. issued_amount = (
  461. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  462. .filter(base, BxAdvanceApply.status == "issued")
  463. .scalar()
  464. or 0
  465. )
  466. m0, m1 = current_month_bounds()
  467. month_apply_amount = (
  468. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  469. .filter(base, BxAdvanceApply.create_time >= m0, BxAdvanceApply.create_time < m1)
  470. .scalar()
  471. or 0
  472. )
  473. month_apply_count = (
  474. db.query(func.count())
  475. .select_from(BxAdvanceApply)
  476. .filter(base, BxAdvanceApply.create_time >= m0, BxAdvanceApply.create_time < m1)
  477. .scalar()
  478. or 0
  479. )
  480. offset_total = (
  481. db.query(func.coalesce(func.sum(BxReimburseOffset.offset_amount), 0))
  482. .join(BxAdvanceApply, BxReimburseOffset.advance_id == BxAdvanceApply.id)
  483. .filter(
  484. BxAdvanceApply.id.in_(db.query(base_q.c.id)),
  485. BxReimburseOffset.tenant_no == tenant_no,
  486. BxReimburseOffset.del_flag == 0,
  487. )
  488. .scalar()
  489. or 0
  490. )
  491. return {
  492. "total_count": int(total_count),
  493. "pending_count": int(pending),
  494. "approved_pass_count": int(approved_pass),
  495. "rejected_count": int(rejected_count),
  496. "pending_issue_count": int(pending_issue),
  497. "issued_count": int(issued),
  498. "issued_amount": float(issued_amount),
  499. "month_apply_amount": float(month_apply_amount),
  500. "month_apply_count": int(month_apply_count),
  501. "offset_total": float(offset_total),
  502. }
  503. @staticmethod
  504. def pending_approval_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict:
  505. subquery = (
  506. db.query(BxAdvanceApply.id)
  507. .join(
  508. BxWorkflowNode,
  509. and_(
  510. BxWorkflowNode.wf_id == BxAdvanceApply.wf_id,
  511. BxWorkflowNode.node_name == BxAdvanceApply.current_node,
  512. BxWorkflowNode.tenant_no == tenant_no,
  513. BxWorkflowNode.del_flag == 0,
  514. ),
  515. )
  516. .filter(
  517. BxAdvanceApply.del_flag == 0,
  518. BxAdvanceApply.tenant_no == tenant_no,
  519. BxAdvanceApply.status == "pending",
  520. BxWorkflowNode.approver_id == user.id,
  521. )
  522. ).subquery()
  523. total = db.query(func.count()).select_from(subquery).scalar() or 0
  524. m0, m1 = current_month_bounds()
  525. month_sum = (
  526. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  527. .join(subquery, BxAdvanceApply.id == subquery.c.id)
  528. .filter(BxAdvanceApply.create_time >= m0, BxAdvanceApply.create_time < m1)
  529. .scalar()
  530. or 0
  531. )
  532. return {"pending_total": int(total), "month_pending_amount": float(month_sum)}
  533. @staticmethod
  534. def offset_amount_map(db: Session, tenant_no: str, advance_ids: List[str]) -> Dict[str, float]:
  535. if not advance_ids:
  536. return {}
  537. rows = (
  538. db.query(
  539. BxReimburseOffset.advance_id,
  540. func.coalesce(func.sum(BxReimburseOffset.offset_amount), 0),
  541. )
  542. .filter(
  543. BxReimburseOffset.advance_id.in_(advance_ids),
  544. BxReimburseOffset.tenant_no == tenant_no,
  545. BxReimburseOffset.del_flag == 0,
  546. )
  547. .group_by(BxReimburseOffset.advance_id)
  548. .all()
  549. )
  550. return {str(advance_id): float(amount) for advance_id, amount in rows}
  551. @staticmethod
  552. def issue(
  553. db: Session,
  554. advance_id: str,
  555. user: BxUserContext,
  556. remark: str = "",
  557. actual_amount: Optional[float] = None,
  558. ) -> Optional[BxAdvanceApply]:
  559. tenant_no = AdvanceService._tenant_no(db, user)
  560. apply_obj = AdvanceService.get_by_id(db, advance_id, tenant_no, user=user)
  561. if not apply_obj:
  562. return None
  563. apply_obj.status = "issued"
  564. apply_obj.update_by = user.id
  565. apply_obj.issue_status = "issued"
  566. apply_obj.issue_time = datetime.now()
  567. apply_obj.issue_user_id = user.id
  568. apply_obj.issue_user_name = user.name
  569. if remark:
  570. apply_obj.issue_remark = remark
  571. if actual_amount is not None:
  572. apply_obj.actual_amount = actual_amount
  573. db.flush()
  574. return apply_obj
  575. @staticmethod
  576. def finance_issue_stats(db: Session, tenant_no: str, *, user: BxUserContext) -> dict:
  577. base_q = (
  578. db.query(BxAdvanceApply)
  579. .filter(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no)
  580. )
  581. base_q = apply_bx_scope(base_q, BxAdvanceApply, user, db, tenant_no).subquery()
  582. base = BxAdvanceApply.id.in_(db.query(base_q.c.id))
  583. pending_f = and_(
  584. BxAdvanceApply.status == "approved",
  585. or_(BxAdvanceApply.issue_status.is_(None), BxAdvanceApply.issue_status != "issued"),
  586. )
  587. pending_n = db.query(func.count()).select_from(BxAdvanceApply).filter(base, pending_f).scalar() or 0
  588. issued_n = (
  589. db.query(func.count())
  590. .select_from(BxAdvanceApply)
  591. .filter(base, BxAdvanceApply.status == "issued")
  592. .scalar()
  593. or 0
  594. )
  595. pending_amount = (
  596. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  597. .filter(base, pending_f)
  598. .scalar()
  599. or 0
  600. )
  601. m0, m1 = current_month_bounds()
  602. amt_expr = func.coalesce(BxAdvanceApply.actual_amount, BxAdvanceApply.amount)
  603. issued_month = (
  604. db.query(func.coalesce(func.sum(amt_expr), 0))
  605. .filter(
  606. base,
  607. BxAdvanceApply.status == "issued",
  608. BxAdvanceApply.issue_time.isnot(None),
  609. BxAdvanceApply.issue_time >= m0,
  610. BxAdvanceApply.issue_time < m1,
  611. )
  612. .scalar()
  613. or 0
  614. )
  615. return {
  616. "pending_count": int(pending_n),
  617. "issued_count": int(issued_n),
  618. "pending_amount": float(pending_amount),
  619. "issued_month_amount": float(issued_month),
  620. }