advance.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. from datetime import datetime, timedelta
  2. from sqlalchemy import and_, func, or_
  3. from sqlalchemy.orm import Session
  4. from app.models.advance import AdvanceApply, AdvanceApproval
  5. from app.models.reimburse import ReimburseOffset
  6. from app.models.workflow import WorkflowDef, WorkflowNode
  7. from app.utils.date_range import current_month_bounds
  8. from app.services.base import CrudService
  9. from app.utils.codegen import generate_apply_code
  10. class AdvanceService:
  11. @staticmethod
  12. def _filter_apply_create_day(query, apply_date: str | None):
  13. """按申请日(create_time 所在自然日)筛选,apply_date 格式 YYYY-MM-DD。"""
  14. if not apply_date or not str(apply_date).strip():
  15. return query
  16. try:
  17. day_start = datetime.strptime(str(apply_date).strip(), "%Y-%m-%d")
  18. day_end = day_start + timedelta(days=1)
  19. except ValueError:
  20. return query
  21. return query.filter(AdvanceApply.create_time >= day_start, AdvanceApply.create_time < day_end)
  22. @staticmethod
  23. def get_by_id(db: Session, id: int) -> AdvanceApply | None:
  24. return CrudService.get_by_id(AdvanceApply, db, id)
  25. @staticmethod
  26. def get_paged(db: Session, page: int, page_size: int, filters: list | None = None):
  27. return CrudService.get_paged(AdvanceApply, db, page, page_size, filters)
  28. @staticmethod
  29. def create(db: Session, obj_in: dict) -> AdvanceApply:
  30. return CrudService.create(AdvanceApply, db, obj_in)
  31. @staticmethod
  32. def update(db: Session, id: int, obj_in: dict) -> AdvanceApply | None:
  33. return CrudService.update(AdvanceApply, db, id, obj_in)
  34. @staticmethod
  35. def soft_delete(db: Session, id: int) -> bool:
  36. return CrudService.soft_delete(AdvanceApply, db, id)
  37. @staticmethod
  38. def pending_approval(
  39. db: Session,
  40. user,
  41. page: int,
  42. page_size: int,
  43. keyword: str = "",
  44. applicant_id: int | None = None,
  45. apply_date: str | None = None,
  46. ):
  47. """查询待当前用户审批的备用金申请(current_node 对应的 WorkflowNode.approver_id == user.id)"""
  48. subquery = (
  49. db.query(AdvanceApply.id)
  50. .join(WorkflowNode, and_(
  51. WorkflowNode.wf_id == AdvanceApply.wf_id,
  52. WorkflowNode.node_name == AdvanceApply.current_node,
  53. ))
  54. .filter(
  55. AdvanceApply.del_flag == 0,
  56. AdvanceApply.status == "pending",
  57. WorkflowNode.approver_id == user.id,
  58. )
  59. )
  60. if keyword:
  61. subquery = subquery.filter(
  62. AdvanceApply.apply_code.contains(keyword) | AdvanceApply.applicant_name.contains(keyword)
  63. )
  64. if applicant_id is not None:
  65. subquery = subquery.filter(AdvanceApply.applicant_id == applicant_id)
  66. subquery = AdvanceService._filter_apply_create_day(subquery, apply_date)
  67. subquery = subquery.subquery()
  68. total = db.query(func.count()).select_from(subquery).scalar()
  69. rows = (
  70. db.query(AdvanceApply)
  71. .join(subquery, AdvanceApply.id == subquery.c.id)
  72. .order_by(AdvanceApply.id.desc())
  73. .offset((page - 1) * page_size)
  74. .limit(page_size)
  75. .all()
  76. )
  77. return rows, total or 0
  78. @staticmethod
  79. def approved_by_me(
  80. db: Session,
  81. user,
  82. page: int,
  83. page_size: int,
  84. keyword: str = "",
  85. applicant_id: int | None = None,
  86. apply_date: str | None = None,
  87. ):
  88. """查询当前用户已审批过的备用金申请"""
  89. subquery = (
  90. db.query(AdvanceApproval.apply_id)
  91. .filter(AdvanceApproval.approver_id == user.id)
  92. .distinct()
  93. .subquery()
  94. )
  95. query = (
  96. db.query(AdvanceApply)
  97. .join(subquery, AdvanceApply.id == subquery.c.apply_id)
  98. .filter(AdvanceApply.del_flag == 0)
  99. )
  100. if keyword:
  101. query = query.filter(
  102. AdvanceApply.apply_code.contains(keyword) | AdvanceApply.applicant_name.contains(keyword)
  103. )
  104. if applicant_id is not None:
  105. query = query.filter(AdvanceApply.applicant_id == applicant_id)
  106. query = AdvanceService._filter_apply_create_day(query, apply_date)
  107. total = query.count()
  108. rows = (
  109. query.order_by(AdvanceApply.id.desc())
  110. .offset((page - 1) * page_size)
  111. .limit(page_size)
  112. .all()
  113. )
  114. return rows, total
  115. @staticmethod
  116. def submit(db: Session, apply_id: int, user) -> AdvanceApply | None:
  117. apply_obj = (
  118. db.query(AdvanceApply)
  119. .filter(AdvanceApply.id == apply_id, AdvanceApply.del_flag == 0)
  120. .first()
  121. )
  122. if not apply_obj:
  123. return None
  124. apply_obj.status = "pending"
  125. if not apply_obj.apply_code:
  126. apply_obj.apply_code = generate_apply_code(db, "BYJ")
  127. apply_obj.update_by = str(user.id)
  128. wf = (
  129. db.query(WorkflowDef)
  130. .filter(WorkflowDef.id == apply_obj.wf_id, WorkflowDef.del_flag == 0)
  131. .first()
  132. )
  133. if wf:
  134. nodes = (
  135. db.query(WorkflowNode)
  136. .filter(WorkflowNode.wf_id == wf.id)
  137. .order_by(WorkflowNode.node_order)
  138. .all()
  139. )
  140. if nodes:
  141. apply_obj.current_node = nodes[0].node_name
  142. db.flush()
  143. return apply_obj
  144. @staticmethod
  145. def approve(db: Session, apply_id: int, approver, opinion: str) -> AdvanceApply | None:
  146. apply_obj = (
  147. db.query(AdvanceApply)
  148. .filter(AdvanceApply.id == apply_id, AdvanceApply.del_flag == 0)
  149. .first()
  150. )
  151. if not apply_obj:
  152. return None
  153. wf = (
  154. db.query(WorkflowDef)
  155. .filter(WorkflowDef.id == apply_obj.wf_id, WorkflowDef.del_flag == 0)
  156. .first()
  157. )
  158. if not wf:
  159. return None
  160. nodes = (
  161. db.query(WorkflowNode)
  162. .filter(WorkflowNode.wf_id == wf.id)
  163. .order_by(WorkflowNode.node_order)
  164. .all()
  165. )
  166. current_idx = None
  167. for i, node in enumerate(nodes):
  168. if node.node_name == apply_obj.current_node:
  169. current_idx = i
  170. break
  171. if current_idx is None:
  172. return None
  173. current_node = nodes[current_idx]
  174. if current_node.approver_id != approver.id:
  175. return None
  176. approval = AdvanceApproval(
  177. apply_id=apply_id,
  178. approver_id=approver.id,
  179. approver_name=approver.username,
  180. approval_opinion=opinion,
  181. node_name=current_node.node_name,
  182. node_order=current_node.node_order,
  183. approval_status="approved",
  184. approval_time=datetime.now(),
  185. )
  186. db.add(approval)
  187. if current_idx == len(nodes) - 1:
  188. apply_obj.status = "approved"
  189. apply_obj.current_node = None
  190. else:
  191. apply_obj.current_node = nodes[current_idx + 1].node_name
  192. apply_obj.update_by = str(approver.id)
  193. db.flush()
  194. return apply_obj
  195. @staticmethod
  196. def reject(db: Session, apply_id: int, approver, opinion: str) -> AdvanceApply | None:
  197. apply_obj = (
  198. db.query(AdvanceApply)
  199. .filter(AdvanceApply.id == apply_id, AdvanceApply.del_flag == 0)
  200. .first()
  201. )
  202. if not apply_obj:
  203. return None
  204. wf = (
  205. db.query(WorkflowDef)
  206. .filter(WorkflowDef.id == apply_obj.wf_id, WorkflowDef.del_flag == 0)
  207. .first()
  208. )
  209. if not wf:
  210. return None
  211. nodes = (
  212. db.query(WorkflowNode)
  213. .filter(WorkflowNode.wf_id == wf.id)
  214. .order_by(WorkflowNode.node_order)
  215. .all()
  216. )
  217. current_node = None
  218. for node in nodes:
  219. if node.node_name == apply_obj.current_node:
  220. current_node = node
  221. break
  222. if current_node is None:
  223. return None
  224. if current_node.approver_id != approver.id:
  225. return None
  226. approval = AdvanceApproval(
  227. apply_id=apply_id,
  228. approver_id=approver.id,
  229. approver_name=approver.username,
  230. approval_opinion=opinion,
  231. node_name=current_node.node_name,
  232. node_order=current_node.node_order,
  233. approval_status="rejected",
  234. approval_time=datetime.now(),
  235. )
  236. db.add(approval)
  237. apply_obj.status = "rejected"
  238. apply_obj.current_node = None
  239. apply_obj.update_by = str(approver.id)
  240. db.flush()
  241. return apply_obj
  242. @staticmethod
  243. def issue(db: Session, apply_id: int, user, remark: str = "", actual_amount: float | None = None) -> AdvanceApply | None:
  244. apply_obj = (
  245. db.query(AdvanceApply)
  246. .filter(AdvanceApply.id == apply_id, AdvanceApply.del_flag == 0)
  247. .first()
  248. )
  249. if not apply_obj:
  250. return None
  251. apply_obj.status = "issued"
  252. apply_obj.update_by = str(user.id)
  253. apply_obj.issue_status = "issued"
  254. apply_obj.issue_time = datetime.now()
  255. apply_obj.issue_user_id = user.id
  256. apply_obj.issue_user_name = user.real_name or user.username
  257. if remark:
  258. apply_obj.issue_remark = remark
  259. if actual_amount is not None:
  260. apply_obj.actual_amount = actual_amount
  261. db.flush()
  262. return apply_obj
  263. @staticmethod
  264. def get_approvals(db: Session, apply_id: int) -> list[dict]:
  265. apply_obj = (
  266. db.query(AdvanceApply)
  267. .filter(AdvanceApply.id == apply_id, AdvanceApply.del_flag == 0)
  268. .first()
  269. )
  270. if not apply_obj or not apply_obj.wf_id:
  271. return []
  272. wf = (
  273. db.query(WorkflowDef)
  274. .filter(WorkflowDef.id == apply_obj.wf_id, WorkflowDef.del_flag == 0)
  275. .first()
  276. )
  277. if not wf:
  278. return []
  279. nodes = (
  280. db.query(WorkflowNode)
  281. .filter(WorkflowNode.wf_id == wf.id)
  282. .order_by(WorkflowNode.node_order)
  283. .all()
  284. )
  285. if not nodes:
  286. return []
  287. approvals = (
  288. db.query(AdvanceApproval)
  289. .filter(AdvanceApproval.apply_id == apply_id)
  290. .all()
  291. )
  292. approval_map = {a.node_order: a for a in approvals}
  293. result = []
  294. for node in nodes:
  295. approval = approval_map.get(node.node_order)
  296. if approval:
  297. result.append({
  298. "id": approval.id,
  299. "advance_id": apply_id,
  300. "node_name": approval.node_name,
  301. "node_order": approval.node_order,
  302. "approval_status": approval.approval_status,
  303. "approval_opinion": approval.approval_opinion,
  304. "approver_name": approval.approver_name,
  305. "approval_time": approval.approval_time,
  306. })
  307. else:
  308. result.append({
  309. "id": 0,
  310. "advance_id": apply_id,
  311. "node_name": node.node_name,
  312. "node_order": node.node_order,
  313. "approval_status": "pending",
  314. "approval_opinion": None,
  315. "approver_name": node.approver_name or "",
  316. "approval_time": None,
  317. })
  318. return result
  319. @staticmethod
  320. def global_status_counts(db: Session) -> dict[str, int]:
  321. rows = (
  322. db.query(AdvanceApply.status, func.count())
  323. .filter(AdvanceApply.del_flag == 0)
  324. .group_by(AdvanceApply.status)
  325. .all()
  326. )
  327. return {str(s or ""): int(c) for s, c in rows}
  328. @staticmethod
  329. def my_apply_stats(db: Session, user_id: int) -> dict:
  330. """我的备用金:全量聚合;待发放=审批通过且财务未标记发放;累计冲抵=本人备用金被冲抵金额之和。"""
  331. base = and_(AdvanceApply.del_flag == 0, AdvanceApply.applicant_id == user_id)
  332. total_count = db.query(func.count()).select_from(AdvanceApply).filter(base).scalar() or 0
  333. pending = db.query(func.count()).select_from(AdvanceApply).filter(base, AdvanceApply.status == "pending").scalar() or 0
  334. approved_pass = (
  335. db.query(func.count())
  336. .select_from(AdvanceApply)
  337. .filter(base, AdvanceApply.status == "approved")
  338. .scalar()
  339. or 0
  340. )
  341. rejected_count = db.query(func.count()).select_from(AdvanceApply).filter(base, AdvanceApply.status == "rejected").scalar() or 0
  342. pending_issue = approved_pass
  343. issued = db.query(func.count()).select_from(AdvanceApply).filter(base, AdvanceApply.status == "issued").scalar() or 0
  344. issued_amount = (
  345. db.query(func.coalesce(func.sum(AdvanceApply.amount), 0)).filter(base, AdvanceApply.status == "issued").scalar() or 0
  346. )
  347. m0, m1 = current_month_bounds()
  348. month_apply_amount = (
  349. db.query(func.coalesce(func.sum(AdvanceApply.amount), 0))
  350. .filter(base, AdvanceApply.create_time >= m0, AdvanceApply.create_time < m1)
  351. .scalar()
  352. or 0
  353. )
  354. month_apply_count = (
  355. db.query(func.count())
  356. .select_from(AdvanceApply)
  357. .filter(base, AdvanceApply.create_time >= m0, AdvanceApply.create_time < m1)
  358. .scalar()
  359. or 0
  360. )
  361. offset_total = (
  362. db.query(func.coalesce(func.sum(ReimburseOffset.offset_amount), 0))
  363. .join(AdvanceApply, ReimburseOffset.advance_id == AdvanceApply.id)
  364. .filter(AdvanceApply.applicant_id == user_id, AdvanceApply.del_flag == 0)
  365. .scalar()
  366. or 0
  367. )
  368. return {
  369. "total_count": int(total_count),
  370. "pending_count": int(pending),
  371. "approved_pass_count": int(approved_pass),
  372. "rejected_count": int(rejected_count),
  373. "pending_issue_count": int(pending_issue),
  374. "issued_count": int(issued),
  375. "issued_amount": float(issued_amount),
  376. "month_apply_amount": float(month_apply_amount),
  377. "month_apply_count": int(month_apply_count),
  378. "offset_total": float(offset_total),
  379. }
  380. @staticmethod
  381. def pending_approval_stats(db: Session, user) -> dict:
  382. """待我审批的备用金。"""
  383. subquery = (
  384. db.query(AdvanceApply.id)
  385. .join(
  386. WorkflowNode,
  387. and_(
  388. WorkflowNode.wf_id == AdvanceApply.wf_id,
  389. WorkflowNode.node_name == AdvanceApply.current_node,
  390. ),
  391. )
  392. .filter(
  393. AdvanceApply.del_flag == 0,
  394. AdvanceApply.status == "pending",
  395. WorkflowNode.approver_id == user.id,
  396. )
  397. ).subquery()
  398. total = db.query(func.count()).select_from(subquery).scalar() or 0
  399. m0, m1 = current_month_bounds()
  400. month_sum = (
  401. db.query(func.coalesce(func.sum(AdvanceApply.amount), 0))
  402. .join(subquery, AdvanceApply.id == subquery.c.id)
  403. .filter(AdvanceApply.create_time >= m0, AdvanceApply.create_time < m1)
  404. .scalar()
  405. or 0
  406. )
  407. return {"pending_total": int(total), "month_pending_amount": float(month_sum)}
  408. @staticmethod
  409. def finance_issue_stats(db: Session) -> dict:
  410. """财务-备用金发放看板。"""
  411. base = AdvanceApply.del_flag == 0
  412. pending_f = and_(
  413. AdvanceApply.status == "approved",
  414. or_(AdvanceApply.issue_status.is_(None), AdvanceApply.issue_status != "issued"),
  415. )
  416. pending_n = db.query(func.count()).filter(base, pending_f).scalar() or 0
  417. issued_n = db.query(func.count()).filter(base, AdvanceApply.status == "issued").scalar() or 0
  418. pending_amount = (
  419. db.query(func.coalesce(func.sum(AdvanceApply.amount), 0)).filter(base, pending_f).scalar() or 0
  420. )
  421. m0, m1 = current_month_bounds()
  422. amt_expr = func.coalesce(AdvanceApply.actual_amount, AdvanceApply.amount)
  423. issued_month = (
  424. db.query(func.coalesce(func.sum(amt_expr), 0))
  425. .filter(
  426. base,
  427. AdvanceApply.status == "issued",
  428. AdvanceApply.issue_time.isnot(None),
  429. AdvanceApply.issue_time >= m0,
  430. AdvanceApply.issue_time < m1,
  431. )
  432. .scalar()
  433. or 0
  434. )
  435. return {
  436. "pending_count": int(pending_n),
  437. "issued_count": int(issued_n),
  438. "pending_amount": float(pending_amount),
  439. "issued_month_amount": float(issued_month),
  440. }