advance.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  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 _approval_record_dict(approval: BxAdvanceApproval, apply_id: str) -> Dict[str, Any]:
  335. return {
  336. "id": approval.id,
  337. "advance_id": apply_id,
  338. "node_name": approval.node_name,
  339. "node_order": approval.node_order,
  340. "approval_status": approval.approval_status,
  341. "approval_opinion": approval.approval_opinion,
  342. "approver_name": approval.approver_name,
  343. "approval_time": approval.approval_time,
  344. }
  345. @staticmethod
  346. def _latest_approval_per_node(
  347. approvals: Sequence[BxAdvanceApproval],
  348. ) -> Dict[int, BxAdvanceApproval]:
  349. """同一节点多次审批时,取 approval_time 最新的一条作为当前节点状态。"""
  350. approval_map: Dict[int, BxAdvanceApproval] = {}
  351. for approval in approvals:
  352. existing = approval_map.get(approval.node_order)
  353. if existing is None:
  354. approval_map[approval.node_order] = approval
  355. continue
  356. approval_time = approval.approval_time or datetime.min
  357. existing_time = existing.approval_time or datetime.min
  358. if approval_time >= existing_time:
  359. approval_map[approval.node_order] = approval
  360. return approval_map
  361. @staticmethod
  362. def _query_approvals(db: Session, apply_id: str, tenant_no: str) -> List[BxAdvanceApproval]:
  363. return (
  364. db.query(BxAdvanceApproval)
  365. .filter(
  366. BxAdvanceApproval.apply_id == apply_id,
  367. BxAdvanceApproval.tenant_no == tenant_no,
  368. BxAdvanceApproval.del_flag == 0,
  369. )
  370. .all()
  371. )
  372. @staticmethod
  373. def get_approvals(
  374. db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
  375. ) -> List[Dict[str, Any]]:
  376. apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
  377. if not apply_obj or not apply_obj.wf_id:
  378. return []
  379. wf = BxCrudService.get_by_id(BxWorkflowDef, db, apply_obj.wf_id, tenant_no)
  380. if not wf:
  381. return []
  382. nodes = AdvanceService._workflow_nodes(db, wf.id, tenant_no)
  383. if not nodes:
  384. return []
  385. approvals = AdvanceService._query_approvals(db, apply_id, tenant_no)
  386. cycle_approvals = filter_current_cycle_approvals(approvals, apply_obj.status)
  387. approval_map = AdvanceService._latest_approval_per_node(cycle_approvals)
  388. result: List[Dict[str, Any]] = []
  389. for node in nodes:
  390. approval = approval_map.get(node.node_order)
  391. if approval:
  392. result.append(AdvanceService._approval_record_dict(approval, apply_id))
  393. else:
  394. result.append(
  395. {
  396. "id": "0",
  397. "advance_id": apply_id,
  398. "node_name": node.node_name,
  399. "node_order": node.node_order,
  400. "approval_status": "pending",
  401. "approval_opinion": None,
  402. "approver_name": node.approver_name or "",
  403. "approval_time": None,
  404. }
  405. )
  406. return result
  407. @staticmethod
  408. def get_approval_history(
  409. db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
  410. ) -> List[Dict[str, Any]]:
  411. """返回全部审批动作,按时间正序排列,用于展示完整审批历史。"""
  412. if not AdvanceService.get_by_id(db, apply_id, tenant_no, user=user):
  413. return []
  414. approvals = (
  415. db.query(BxAdvanceApproval)
  416. .filter(
  417. BxAdvanceApproval.apply_id == apply_id,
  418. BxAdvanceApproval.tenant_no == tenant_no,
  419. BxAdvanceApproval.del_flag == 0,
  420. )
  421. .order_by(BxAdvanceApproval.approval_time.asc(), BxAdvanceApproval.create_time.asc())
  422. .all()
  423. )
  424. return [AdvanceService._approval_record_dict(a, apply_id) for a in approvals]
  425. @staticmethod
  426. def global_status_counts(db: Session, tenant_no: str, *, user: BxUserContext) -> Dict[str, int]:
  427. query = db.query(BxAdvanceApply.status, func.count()).filter(
  428. BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no
  429. )
  430. query = apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
  431. rows = query.group_by(BxAdvanceApply.status).all()
  432. return {str(s or ""): int(c) for s, c in rows}
  433. @staticmethod
  434. def _my_apply_base(db: Session, user: BxUserContext, tenant_no: str):
  435. query = db.query(BxAdvanceApply).filter(
  436. BxAdvanceApply.del_flag == 0,
  437. BxAdvanceApply.tenant_no == tenant_no,
  438. or_(
  439. BxAdvanceApply.applicant_id == user.id,
  440. BxAdvanceApply.create_by == user.id,
  441. ),
  442. )
  443. return apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
  444. @staticmethod
  445. def my_apply_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict:
  446. base_q = AdvanceService._my_apply_base(db, user, tenant_no).subquery()
  447. base = BxAdvanceApply.id.in_(db.query(base_q.c.id))
  448. total_count = (
  449. db.query(func.count()).select_from(BxAdvanceApply).filter(base).scalar() or 0
  450. )
  451. pending = (
  452. db.query(func.count())
  453. .select_from(BxAdvanceApply)
  454. .filter(base, BxAdvanceApply.status == "pending")
  455. .scalar()
  456. or 0
  457. )
  458. approved_pass = (
  459. db.query(func.count())
  460. .select_from(BxAdvanceApply)
  461. .filter(base, BxAdvanceApply.status == "approved")
  462. .scalar()
  463. or 0
  464. )
  465. rejected_count = (
  466. db.query(func.count())
  467. .select_from(BxAdvanceApply)
  468. .filter(base, BxAdvanceApply.status == "rejected")
  469. .scalar()
  470. or 0
  471. )
  472. pending_issue = approved_pass
  473. issued = (
  474. db.query(func.count())
  475. .select_from(BxAdvanceApply)
  476. .filter(base, BxAdvanceApply.status == "issued")
  477. .scalar()
  478. or 0
  479. )
  480. issued_amount = (
  481. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  482. .filter(base, BxAdvanceApply.status == "issued")
  483. .scalar()
  484. or 0
  485. )
  486. m0, m1 = current_month_bounds()
  487. month_apply_amount = (
  488. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  489. .filter(base, BxAdvanceApply.create_time >= m0, BxAdvanceApply.create_time < m1)
  490. .scalar()
  491. or 0
  492. )
  493. month_apply_count = (
  494. db.query(func.count())
  495. .select_from(BxAdvanceApply)
  496. .filter(base, BxAdvanceApply.create_time >= m0, BxAdvanceApply.create_time < m1)
  497. .scalar()
  498. or 0
  499. )
  500. offset_total = (
  501. db.query(func.coalesce(func.sum(BxReimburseOffset.offset_amount), 0))
  502. .join(BxAdvanceApply, BxReimburseOffset.advance_id == BxAdvanceApply.id)
  503. .filter(
  504. BxAdvanceApply.id.in_(db.query(base_q.c.id)),
  505. BxReimburseOffset.tenant_no == tenant_no,
  506. BxReimburseOffset.del_flag == 0,
  507. )
  508. .scalar()
  509. or 0
  510. )
  511. return {
  512. "total_count": int(total_count),
  513. "pending_count": int(pending),
  514. "approved_pass_count": int(approved_pass),
  515. "rejected_count": int(rejected_count),
  516. "pending_issue_count": int(pending_issue),
  517. "issued_count": int(issued),
  518. "issued_amount": float(issued_amount),
  519. "month_apply_amount": float(month_apply_amount),
  520. "month_apply_count": int(month_apply_count),
  521. "offset_total": float(offset_total),
  522. }
  523. @staticmethod
  524. def pending_approval_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict:
  525. subquery = (
  526. db.query(BxAdvanceApply.id)
  527. .join(
  528. BxWorkflowNode,
  529. and_(
  530. BxWorkflowNode.wf_id == BxAdvanceApply.wf_id,
  531. BxWorkflowNode.node_name == BxAdvanceApply.current_node,
  532. BxWorkflowNode.tenant_no == tenant_no,
  533. BxWorkflowNode.del_flag == 0,
  534. ),
  535. )
  536. .filter(
  537. BxAdvanceApply.del_flag == 0,
  538. BxAdvanceApply.tenant_no == tenant_no,
  539. BxAdvanceApply.status == "pending",
  540. BxWorkflowNode.approver_id == user.id,
  541. )
  542. ).subquery()
  543. total = db.query(func.count()).select_from(subquery).scalar() or 0
  544. m0, m1 = current_month_bounds()
  545. month_sum = (
  546. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  547. .join(subquery, BxAdvanceApply.id == subquery.c.id)
  548. .filter(BxAdvanceApply.create_time >= m0, BxAdvanceApply.create_time < m1)
  549. .scalar()
  550. or 0
  551. )
  552. return {"pending_total": int(total), "month_pending_amount": float(month_sum)}
  553. @staticmethod
  554. def offset_amount_map(db: Session, tenant_no: str, advance_ids: List[str]) -> Dict[str, float]:
  555. if not advance_ids:
  556. return {}
  557. rows = (
  558. db.query(
  559. BxReimburseOffset.advance_id,
  560. func.coalesce(func.sum(BxReimburseOffset.offset_amount), 0),
  561. )
  562. .filter(
  563. BxReimburseOffset.advance_id.in_(advance_ids),
  564. BxReimburseOffset.tenant_no == tenant_no,
  565. BxReimburseOffset.del_flag == 0,
  566. )
  567. .group_by(BxReimburseOffset.advance_id)
  568. .all()
  569. )
  570. return {str(advance_id): float(amount) for advance_id, amount in rows}
  571. @staticmethod
  572. def issue(
  573. db: Session,
  574. advance_id: str,
  575. user: BxUserContext,
  576. remark: str = "",
  577. actual_amount: Optional[float] = None,
  578. ) -> Optional[BxAdvanceApply]:
  579. tenant_no = AdvanceService._tenant_no(db, user)
  580. apply_obj = AdvanceService.get_by_id(db, advance_id, tenant_no, user=user)
  581. if not apply_obj:
  582. return None
  583. apply_obj.status = "issued"
  584. apply_obj.update_by = user.id
  585. apply_obj.issue_status = "issued"
  586. apply_obj.issue_time = datetime.now()
  587. apply_obj.issue_user_id = user.id
  588. apply_obj.issue_user_name = user.name
  589. if remark:
  590. apply_obj.issue_remark = remark
  591. if actual_amount is not None:
  592. apply_obj.actual_amount = actual_amount
  593. db.flush()
  594. return apply_obj
  595. @staticmethod
  596. def finance_issue_stats(db: Session, tenant_no: str, *, user: BxUserContext) -> dict:
  597. base_q = (
  598. db.query(BxAdvanceApply)
  599. .filter(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no)
  600. )
  601. base_q = apply_bx_scope(base_q, BxAdvanceApply, user, db, tenant_no).subquery()
  602. base = BxAdvanceApply.id.in_(db.query(base_q.c.id))
  603. pending_f = and_(
  604. BxAdvanceApply.status == "approved",
  605. or_(BxAdvanceApply.issue_status.is_(None), BxAdvanceApply.issue_status != "issued"),
  606. )
  607. pending_n = db.query(func.count()).select_from(BxAdvanceApply).filter(base, pending_f).scalar() or 0
  608. issued_n = (
  609. db.query(func.count())
  610. .select_from(BxAdvanceApply)
  611. .filter(base, BxAdvanceApply.status == "issued")
  612. .scalar()
  613. or 0
  614. )
  615. pending_amount = (
  616. db.query(func.coalesce(func.sum(BxAdvanceApply.amount), 0))
  617. .filter(base, pending_f)
  618. .scalar()
  619. or 0
  620. )
  621. m0, m1 = current_month_bounds()
  622. amt_expr = func.coalesce(BxAdvanceApply.actual_amount, BxAdvanceApply.amount)
  623. issued_month = (
  624. db.query(func.coalesce(func.sum(amt_expr), 0))
  625. .filter(
  626. base,
  627. BxAdvanceApply.status == "issued",
  628. BxAdvanceApply.issue_time.isnot(None),
  629. BxAdvanceApply.issue_time >= m0,
  630. BxAdvanceApply.issue_time < m1,
  631. )
  632. .scalar()
  633. or 0
  634. )
  635. return {
  636. "pending_count": int(pending_n),
  637. "issued_count": int(issued_n),
  638. "pending_amount": float(pending_amount),
  639. "issued_month_amount": float(issued_month),
  640. }