advance.py 25 KB

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