advance.py 26 KB

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