advance.py 17 KB

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