crm_contract_sub_routes.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. """CRM 合同子模块路由 — 变更/模板/审批独立页"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from typing import Optional
  5. from fastapi import Depends, Query, Request
  6. from sqlalchemy.orm import Session
  7. from app.core.database import get_db
  8. from app.core.menu_guard import require_admin_or_super_admin
  9. from app.core.response import error_response, success_response
  10. from app.core.tenant_context import get_tenant_no
  11. from app.crud.crm.data_scope_helpers import crm_user_info
  12. from app.crud.crm.contract import (
  13. create_contract_change,
  14. create_contract_template,
  15. export_contract_changes,
  16. export_contract_templates,
  17. execute_change,
  18. get_contract_change_detail,
  19. get_contract_template_detail,
  20. list_contract_change_column_facet_values,
  21. list_contract_changes,
  22. list_contract_template_column_facet_values,
  23. list_contract_templates,
  24. soft_delete_contract_changes,
  25. soft_delete_contract_templates,
  26. submit_change_approval,
  27. toggle_contract_template_active,
  28. total_pages,
  29. update_contract_change,
  30. update_contract_template,
  31. withdraw_change,
  32. )
  33. from app.crud.crm.contract_approval import (
  34. approve_pass,
  35. approve_reject,
  36. approve_transfer,
  37. export_contract_approvals,
  38. get_contract_approval_detail,
  39. list_contract_approval_column_facet_values,
  40. list_contract_approvals_page,
  41. refresh_approver_names,
  42. )
  43. from app.schemas.crm.contract import (
  44. ContractApprovalColumnFacetBody,
  45. ContractApprovalExportBody,
  46. ContractApprovalListQuery,
  47. ContractApprovalPassBody,
  48. ContractApprovalRefreshNamesBody,
  49. ContractApprovalRejectBody,
  50. ContractApprovalTransferBody,
  51. ContractChangeColumnFacetBody,
  52. ContractChangeCreateBody,
  53. ContractChangeExportBody,
  54. ContractChangeListQuery,
  55. ContractChangeSubmitBody,
  56. ContractChangeUpdateBody,
  57. ContractTemplateColumnFacetBody,
  58. ContractTemplateCreateBody,
  59. ContractTemplateExportBody,
  60. ContractTemplateListQuery,
  61. ContractTemplateUpdateBody,
  62. )
  63. from app.utils.crm_export_columns import (
  64. CONTRACT_APPROVAL_EXPORT_COLUMNS,
  65. CONTRACT_CHANGE_EXPORT_COLUMNS,
  66. CONTRACT_TEMPLATE_EXPORT_COLUMNS,
  67. )
  68. from app.utils.excel_export import (
  69. ExportValidationError,
  70. export_rows_to_xlsx_response,
  71. filter_export_columns,
  72. )
  73. from app.api.crm import _filter_export_rows_by_ids
  74. def register_contract_sub_routes(router):
  75. # --- 合同变更 ---
  76. @router.get("/contract/change/list")
  77. async def contract_change_list(
  78. request: Request,
  79. pageNum: int = Query(1, ge=1),
  80. pageSize: int = Query(10, ge=1, le=500),
  81. contractId: Optional[str] = None,
  82. contractNo: Optional[str] = None,
  83. contractName: Optional[str] = None,
  84. changeType: Optional[str] = None,
  85. approvalStatus: Optional[str] = None,
  86. beginTime: Optional[str] = None,
  87. endTime: Optional[str] = None,
  88. _user: dict = Depends(require_admin_or_super_admin),
  89. db: Session = Depends(get_db),
  90. ):
  91. try:
  92. tenant_no = get_tenant_no(db, _user)
  93. user_info = crm_user_info(_user)
  94. query = ContractChangeListQuery(
  95. pageNum=pageNum,
  96. pageSize=pageSize,
  97. contractId=contractId,
  98. contractNo=contractNo,
  99. contractName=contractName,
  100. changeType=changeType,
  101. approvalStatus=approvalStatus,
  102. beginTime=beginTime,
  103. endTime=endTime,
  104. )
  105. filters = query.model_dump(
  106. by_alias=True,
  107. exclude={"pageNum", "pageSize"},
  108. exclude_none=True,
  109. )
  110. if query.model_extra:
  111. filters.update(
  112. {
  113. k: v
  114. for k, v in query.model_extra.items()
  115. if v is not None and str(v).strip() != ""
  116. }
  117. )
  118. # 列头漏斗等未在签名中声明的 query 参数一并透传
  119. skip_keys = {"pageNum", "pageSize", *filters.keys()}
  120. for key, val in request.query_params.multi_items():
  121. if key in skip_keys or val is None or str(val).strip() == "":
  122. continue
  123. filters[key] = val
  124. rows, total = list_contract_changes(
  125. db,
  126. tenant_no,
  127. user_info=user_info,
  128. page=query.pageNum,
  129. page_size=query.pageSize,
  130. filters=filters,
  131. )
  132. return success_response(
  133. {
  134. "list": rows,
  135. "rows": rows,
  136. "total": total,
  137. "pageNum": query.pageNum,
  138. "pageSize": query.pageSize,
  139. "totalPages": total_pages(total, query.pageSize),
  140. }
  141. )
  142. except ValueError as e:
  143. return error_response(400, str(e))
  144. except Exception as e:
  145. return error_response(500, str(e))
  146. @router.post("/contract/change/column-facet-values")
  147. async def contract_change_column_facet_values(
  148. body: ContractChangeColumnFacetBody,
  149. _user: dict = Depends(require_admin_or_super_admin),
  150. db: Session = Depends(get_db),
  151. ):
  152. """合同变更列表列头可选值(合同编号/名称去重;Excel 级联上下文)"""
  153. try:
  154. from app.services.crm.facet_context import facet_filter_kwargs_from_body
  155. tenant_no = get_tenant_no(db, _user)
  156. user_info = crm_user_info(_user)
  157. filter_kwargs = facet_filter_kwargs_from_body(body, body.field)
  158. rows = list_contract_change_column_facet_values(
  159. db,
  160. tenant_no,
  161. field=body.field,
  162. keyword=body.keyword,
  163. limit=body.limit if body.limit is not None else 500,
  164. user_info=user_info,
  165. filter_kwargs=filter_kwargs,
  166. )
  167. return success_response({"list": rows})
  168. except ValueError as e:
  169. return error_response(400, str(e))
  170. except Exception as e:
  171. return error_response(500, str(e))
  172. @router.get("/contract/change/{change_id}")
  173. async def contract_change_detail(
  174. change_id: str,
  175. _user: dict = Depends(require_admin_or_super_admin),
  176. db: Session = Depends(get_db),
  177. ):
  178. try:
  179. tenant_no = get_tenant_no(db, _user)
  180. user_info = crm_user_info(_user)
  181. detail = get_contract_change_detail(db, change_id, tenant_no=tenant_no, user_info=user_info)
  182. return success_response(detail)
  183. except ValueError as e:
  184. return error_response(400, str(e))
  185. except Exception as e:
  186. return error_response(500, str(e))
  187. @router.post("/contract/change")
  188. async def contract_change_add(
  189. body: ContractChangeCreateBody,
  190. _user: dict = Depends(require_admin_or_super_admin),
  191. db: Session = Depends(get_db),
  192. ):
  193. try:
  194. tenant_no = get_tenant_no(db, _user)
  195. user_info = crm_user_info(_user)
  196. user_id = _user.get("user_id", "") or _user.get("id", "")
  197. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  198. detail = create_contract_change(
  199. db,
  200. tenant_no=tenant_no,
  201. data=body.model_dump(by_alias=True),
  202. user_id=user_id,
  203. dept_id=dept_id,
  204. )
  205. return success_response(detail["id"], message="新增成功")
  206. except ValueError as e:
  207. return error_response(400, str(e))
  208. except Exception as e:
  209. return error_response(500, str(e))
  210. @router.put("/contract/change")
  211. async def contract_change_edit(
  212. body: ContractChangeUpdateBody,
  213. _user: dict = Depends(require_admin_or_super_admin),
  214. db: Session = Depends(get_db),
  215. ):
  216. try:
  217. tenant_no = get_tenant_no(db, _user)
  218. user_info = crm_user_info(_user)
  219. user_id = _user.get("user_id", "") or _user.get("id", "")
  220. data = body.model_dump(by_alias=True)
  221. change_id = data.pop("id")
  222. detail = update_contract_change(
  223. db,
  224. change_id,
  225. tenant_no=tenant_no,
  226. data=data,
  227. user_id=user_id,
  228. user_info=user_info,
  229. )
  230. return success_response(detail, message="修改成功")
  231. except ValueError as e:
  232. return error_response(400, str(e))
  233. except Exception as e:
  234. return error_response(500, str(e))
  235. @router.delete("/contract/change/{ids}")
  236. async def contract_change_remove(
  237. ids: str,
  238. _user: dict = Depends(require_admin_or_super_admin),
  239. db: Session = Depends(get_db),
  240. ):
  241. try:
  242. tenant_no = get_tenant_no(db, _user)
  243. user_info = crm_user_info(_user)
  244. id_list = [x.strip() for x in ids.split(",") if x.strip()]
  245. soft_delete_contract_changes(db, id_list, tenant_no=tenant_no, user_info=user_info)
  246. return success_response(None, message="删除成功")
  247. except ValueError as e:
  248. return error_response(400, str(e))
  249. except Exception as e:
  250. return error_response(500, str(e))
  251. @router.post("/contract/change/submitApproval")
  252. async def contract_change_submit_approval(
  253. body: ContractChangeSubmitBody,
  254. _user: dict = Depends(require_admin_or_super_admin),
  255. db: Session = Depends(get_db),
  256. ):
  257. try:
  258. tenant_no = get_tenant_no(db, _user)
  259. user_info = crm_user_info(_user)
  260. user_id = _user.get("user_id", "") or _user.get("id", "")
  261. submit_change_approval(
  262. db,
  263. body.id,
  264. tenant_no=tenant_no,
  265. approver_ids=body.approverIds,
  266. remark=body.remark or "",
  267. user_id=user_id,
  268. )
  269. return success_response(None, message="提交成功")
  270. except ValueError as e:
  271. return error_response(400, str(e))
  272. except Exception as e:
  273. return error_response(500, str(e))
  274. @router.put("/contract/change/withdraw/{change_id}")
  275. async def contract_change_withdraw(
  276. change_id: str,
  277. _user: dict = Depends(require_admin_or_super_admin),
  278. db: Session = Depends(get_db),
  279. ):
  280. try:
  281. tenant_no = get_tenant_no(db, _user)
  282. user_info = crm_user_info(_user)
  283. user_id = _user.get("user_id", "") or _user.get("id", "")
  284. withdraw_change(db, change_id, tenant_no=tenant_no, user_id=user_id)
  285. return success_response(None, message="撤回成功")
  286. except ValueError as e:
  287. return error_response(400, str(e))
  288. except Exception as e:
  289. return error_response(500, str(e))
  290. @router.put("/contract/change/execute/{change_id}")
  291. async def contract_change_execute(
  292. change_id: str,
  293. _user: dict = Depends(require_admin_or_super_admin),
  294. db: Session = Depends(get_db),
  295. ):
  296. try:
  297. tenant_no = get_tenant_no(db, _user)
  298. user_info = crm_user_info(_user)
  299. user_id = _user.get("user_id", "") or _user.get("id", "")
  300. execute_change(db, change_id, tenant_no=tenant_no, user_id=user_id)
  301. return success_response(None, message="执行成功")
  302. except ValueError as e:
  303. return error_response(400, str(e))
  304. except Exception as e:
  305. return error_response(500, str(e))
  306. @router.post("/contract/change/export")
  307. async def contract_change_export(
  308. body: ContractChangeExportBody = None,
  309. _user: dict = Depends(require_admin_or_super_admin),
  310. db: Session = Depends(get_db),
  311. ):
  312. try:
  313. tenant_no = get_tenant_no(db, _user)
  314. user_info = crm_user_info(_user)
  315. list_body = body or ContractChangeExportBody()
  316. filters = list_body.model_dump(
  317. by_alias=True,
  318. exclude={"pageNum", "pageSize"},
  319. exclude_none=True,
  320. )
  321. if list_body.model_extra:
  322. filters.update(
  323. {
  324. k: v
  325. for k, v in list_body.model_extra.items()
  326. if v is not None and str(v).strip() != ""
  327. }
  328. )
  329. rows = export_contract_changes(db, tenant_no, filters=filters)
  330. rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None) or getattr(body, "ids", None))
  331. export_cols = filter_export_columns(CONTRACT_CHANGE_EXPORT_COLUMNS, getattr(list_body, "columns", None) or getattr(body, "columns", None))
  332. return export_rows_to_xlsx_response(
  333. db,
  334. tenant_no=tenant_no,
  335. sheet_title="合同变更记录",
  336. columns=export_cols,
  337. rows=rows,
  338. filename_stem=f"合同变更_{datetime.now().strftime('%Y%m%d%H%M%S')}",
  339. include_legacy_filename=False,
  340. )
  341. except ValueError as e:
  342. return error_response(400, str(e))
  343. except Exception as e:
  344. return error_response(500, str(e))
  345. # --- 合同模板库 ---
  346. @router.get("/contract/template/list")
  347. async def contract_template_list(
  348. request: Request,
  349. pageNum: int = Query(1, ge=1),
  350. pageSize: int = Query(10, ge=1, le=500),
  351. templateNo: Optional[str] = None,
  352. templateName: Optional[str] = None,
  353. templateType: Optional[str] = None,
  354. isActive: Optional[int] = None,
  355. beginTime: Optional[str] = None,
  356. endTime: Optional[str] = None,
  357. _user: dict = Depends(require_admin_or_super_admin),
  358. db: Session = Depends(get_db),
  359. ):
  360. try:
  361. tenant_no = get_tenant_no(db, _user)
  362. user_info = crm_user_info(_user)
  363. query = ContractTemplateListQuery(
  364. pageNum=pageNum,
  365. pageSize=pageSize,
  366. templateNo=templateNo,
  367. templateName=templateName,
  368. templateType=templateType,
  369. isActive=isActive,
  370. beginTime=beginTime,
  371. endTime=endTime,
  372. )
  373. filters = query.model_dump(
  374. by_alias=True,
  375. exclude={"pageNum", "pageSize"},
  376. exclude_none=True,
  377. )
  378. if query.model_extra:
  379. filters.update(
  380. {
  381. k: v
  382. for k, v in query.model_extra.items()
  383. if v is not None and str(v).strip() != ""
  384. }
  385. )
  386. skip_keys = {"pageNum", "pageSize", *filters.keys()}
  387. for key, val in request.query_params.multi_items():
  388. if key in skip_keys or val is None or str(val).strip() == "":
  389. continue
  390. filters[key] = val
  391. rows, total = list_contract_templates(
  392. db,
  393. tenant_no,
  394. user_info=user_info,
  395. page=query.pageNum,
  396. page_size=query.pageSize,
  397. filters=filters,
  398. )
  399. return success_response(
  400. {
  401. "list": rows,
  402. "rows": rows,
  403. "total": total,
  404. "pageNum": query.pageNum,
  405. "pageSize": query.pageSize,
  406. "totalPages": total_pages(total, query.pageSize),
  407. }
  408. )
  409. except ValueError as e:
  410. return error_response(400, str(e))
  411. except Exception as e:
  412. return error_response(500, str(e))
  413. @router.post("/contract/template/column-facet-values")
  414. async def contract_template_column_facet_values(
  415. body: ContractTemplateColumnFacetBody,
  416. _user: dict = Depends(require_admin_or_super_admin),
  417. db: Session = Depends(get_db),
  418. ):
  419. """合同模板列表列头可选值(编号/名称去重;Excel 级联上下文)"""
  420. try:
  421. from app.services.crm.facet_context import facet_filter_kwargs_from_body
  422. tenant_no = get_tenant_no(db, _user)
  423. user_info = crm_user_info(_user)
  424. filter_kwargs = facet_filter_kwargs_from_body(body, body.field)
  425. rows = list_contract_template_column_facet_values(
  426. db,
  427. tenant_no,
  428. field=body.field,
  429. keyword=body.keyword,
  430. limit=body.limit if body.limit is not None else 500,
  431. user_info=user_info,
  432. filter_kwargs=filter_kwargs,
  433. )
  434. return success_response({"list": rows})
  435. except ValueError as e:
  436. return error_response(400, str(e))
  437. except Exception as e:
  438. return error_response(500, str(e))
  439. @router.get("/contract/template/{template_id}")
  440. async def contract_template_detail(
  441. template_id: str,
  442. _user: dict = Depends(require_admin_or_super_admin),
  443. db: Session = Depends(get_db),
  444. ):
  445. try:
  446. tenant_no = get_tenant_no(db, _user)
  447. user_info = crm_user_info(_user)
  448. detail = get_contract_template_detail(db, template_id, tenant_no=tenant_no)
  449. return success_response(detail)
  450. except ValueError as e:
  451. return error_response(400, str(e))
  452. except Exception as e:
  453. return error_response(500, str(e))
  454. @router.post("/contract/template")
  455. async def contract_template_add(
  456. body: ContractTemplateCreateBody,
  457. _user: dict = Depends(require_admin_or_super_admin),
  458. db: Session = Depends(get_db),
  459. ):
  460. try:
  461. tenant_no = get_tenant_no(db, _user)
  462. user_info = crm_user_info(_user)
  463. user_id = _user.get("user_id", "") or _user.get("id", "")
  464. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  465. detail = create_contract_template(
  466. db,
  467. tenant_no=tenant_no,
  468. data=body.model_dump(by_alias=True),
  469. user_id=user_id,
  470. dept_id=dept_id,
  471. )
  472. return success_response(detail["id"], message="新增成功")
  473. except ValueError as e:
  474. return error_response(400, str(e))
  475. except Exception as e:
  476. return error_response(500, str(e))
  477. @router.put("/contract/template")
  478. async def contract_template_edit(
  479. body: ContractTemplateUpdateBody,
  480. _user: dict = Depends(require_admin_or_super_admin),
  481. db: Session = Depends(get_db),
  482. ):
  483. try:
  484. tenant_no = get_tenant_no(db, _user)
  485. user_info = crm_user_info(_user)
  486. user_id = _user.get("user_id", "") or _user.get("id", "")
  487. data = body.model_dump(by_alias=True)
  488. template_id = data.pop("id")
  489. detail = update_contract_template(
  490. db,
  491. template_id,
  492. tenant_no=tenant_no,
  493. data=data,
  494. user_id=user_id,
  495. user_info=user_info,
  496. )
  497. return success_response(detail, message="修改成功")
  498. except ValueError as e:
  499. return error_response(400, str(e))
  500. except Exception as e:
  501. return error_response(500, str(e))
  502. @router.delete("/contract/template/{ids}")
  503. async def contract_template_remove(
  504. ids: str,
  505. _user: dict = Depends(require_admin_or_super_admin),
  506. db: Session = Depends(get_db),
  507. ):
  508. try:
  509. tenant_no = get_tenant_no(db, _user)
  510. user_info = crm_user_info(_user)
  511. id_list = [x.strip() for x in ids.split(",") if x.strip()]
  512. soft_delete_contract_templates(db, id_list, tenant_no=tenant_no, user_info=user_info)
  513. return success_response(None, message="删除成功")
  514. except ValueError as e:
  515. return error_response(400, str(e))
  516. except Exception as e:
  517. return error_response(500, str(e))
  518. @router.put("/contract/template/enable/{template_id}")
  519. async def contract_template_enable(
  520. template_id: str,
  521. _user: dict = Depends(require_admin_or_super_admin),
  522. db: Session = Depends(get_db),
  523. ):
  524. try:
  525. tenant_no = get_tenant_no(db, _user)
  526. user_info = crm_user_info(_user)
  527. user_id = _user.get("user_id", "") or _user.get("id", "")
  528. toggle_contract_template_active(
  529. db, template_id, tenant_no=tenant_no, is_active=1, user_id=user_id
  530. )
  531. return success_response(None, message="启用成功")
  532. except ValueError as e:
  533. return error_response(400, str(e))
  534. except Exception as e:
  535. return error_response(500, str(e))
  536. @router.put("/contract/template/disable/{template_id}")
  537. async def contract_template_disable(
  538. template_id: str,
  539. _user: dict = Depends(require_admin_or_super_admin),
  540. db: Session = Depends(get_db),
  541. ):
  542. try:
  543. tenant_no = get_tenant_no(db, _user)
  544. user_info = crm_user_info(_user)
  545. user_id = _user.get("user_id", "") or _user.get("id", "")
  546. toggle_contract_template_active(
  547. db, template_id, tenant_no=tenant_no, is_active=0, user_id=user_id
  548. )
  549. return success_response(None, message="停用成功")
  550. except ValueError as e:
  551. return error_response(400, str(e))
  552. except Exception as e:
  553. return error_response(500, str(e))
  554. @router.post("/contract/template/export")
  555. async def contract_template_export(
  556. body: ContractTemplateExportBody = None,
  557. _user: dict = Depends(require_admin_or_super_admin),
  558. db: Session = Depends(get_db),
  559. ):
  560. try:
  561. tenant_no = get_tenant_no(db, _user)
  562. user_info = crm_user_info(_user)
  563. list_body = body or ContractTemplateExportBody()
  564. filters = list_body.model_dump(
  565. by_alias=True,
  566. exclude={"pageNum", "pageSize"},
  567. exclude_none=True,
  568. )
  569. if list_body.model_extra:
  570. filters.update(
  571. {
  572. k: v
  573. for k, v in list_body.model_extra.items()
  574. if v is not None and str(v).strip() != ""
  575. }
  576. )
  577. rows = export_contract_templates(db, tenant_no, filters=filters)
  578. rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None) or getattr(body, "ids", None))
  579. export_cols = filter_export_columns(CONTRACT_TEMPLATE_EXPORT_COLUMNS, getattr(list_body, "columns", None) or getattr(body, "columns", None))
  580. return export_rows_to_xlsx_response(
  581. db,
  582. tenant_no=tenant_no,
  583. sheet_title="合同模板",
  584. columns=export_cols,
  585. rows=rows,
  586. filename_stem=f"合同模板_{datetime.now().strftime('%Y%m%d%H%M%S')}",
  587. include_legacy_filename=False,
  588. )
  589. except ValueError as e:
  590. return error_response(400, str(e))
  591. except Exception as e:
  592. return error_response(500, str(e))
  593. # --- 审批记录独立页 ---
  594. @router.get("/contract/approval/list")
  595. async def contract_approval_list(
  596. request: Request,
  597. pageNum: int = Query(1, ge=1),
  598. pageSize: int = Query(10, ge=1, le=500),
  599. contractId: Optional[str] = None,
  600. contractNo: Optional[str] = None,
  601. contractName: Optional[str] = None,
  602. approverName: Optional[str] = None,
  603. approvalResult: Optional[str] = None,
  604. beginTime: Optional[str] = None,
  605. endTime: Optional[str] = None,
  606. _user: dict = Depends(require_admin_or_super_admin),
  607. db: Session = Depends(get_db),
  608. ):
  609. try:
  610. tenant_no = get_tenant_no(db, _user)
  611. user_info = crm_user_info(_user)
  612. query = ContractApprovalListQuery(
  613. pageNum=pageNum,
  614. pageSize=pageSize,
  615. contractId=contractId,
  616. contractNo=contractNo,
  617. contractName=contractName,
  618. approverName=approverName,
  619. approvalResult=approvalResult,
  620. beginTime=beginTime,
  621. endTime=endTime,
  622. )
  623. filters = query.model_dump(
  624. by_alias=True,
  625. exclude={"pageNum", "pageSize"},
  626. exclude_none=True,
  627. )
  628. if query.model_extra:
  629. filters.update(
  630. {
  631. k: v
  632. for k, v in query.model_extra.items()
  633. if v is not None and str(v).strip() != ""
  634. }
  635. )
  636. skip_keys = {"pageNum", "pageSize", *filters.keys()}
  637. for key, val in request.query_params.multi_items():
  638. if key in skip_keys or val is None or str(val).strip() == "":
  639. continue
  640. filters[key] = val
  641. rows, total = list_contract_approvals_page(
  642. db,
  643. tenant_no,
  644. page=query.pageNum,
  645. page_size=query.pageSize,
  646. filters=filters,
  647. )
  648. return success_response(
  649. {
  650. "list": rows,
  651. "rows": rows,
  652. "total": total,
  653. "pageNum": query.pageNum,
  654. "pageSize": query.pageSize,
  655. "totalPages": total_pages(total, query.pageSize),
  656. }
  657. )
  658. except ValueError as e:
  659. return error_response(400, str(e))
  660. except Exception as e:
  661. return error_response(500, str(e))
  662. @router.post("/contract/approval/column-facet-values")
  663. async def contract_approval_column_facet_values(
  664. body: ContractApprovalColumnFacetBody,
  665. _user: dict = Depends(require_admin_or_super_admin),
  666. db: Session = Depends(get_db),
  667. ):
  668. """合同审批记录列头可选值(合同编号/名称去重;Excel 级联上下文)"""
  669. try:
  670. from app.services.crm.facet_context import facet_filter_kwargs_from_body
  671. tenant_no = get_tenant_no(db, _user)
  672. filter_kwargs = facet_filter_kwargs_from_body(body, body.field)
  673. rows = list_contract_approval_column_facet_values(
  674. db,
  675. tenant_no,
  676. field=body.field,
  677. keyword=body.keyword,
  678. limit=body.limit if body.limit is not None else 500,
  679. filter_kwargs=filter_kwargs,
  680. )
  681. return success_response({"list": rows})
  682. except ValueError as e:
  683. return error_response(400, str(e))
  684. except Exception as e:
  685. return error_response(500, str(e))
  686. @router.get("/contract/approval/{approval_id}")
  687. async def contract_approval_detail(
  688. approval_id: str,
  689. _user: dict = Depends(require_admin_or_super_admin),
  690. db: Session = Depends(get_db),
  691. ):
  692. try:
  693. tenant_no = get_tenant_no(db, _user)
  694. user_info = crm_user_info(_user)
  695. detail = get_contract_approval_detail(db, approval_id, tenant_no=tenant_no)
  696. return success_response(detail)
  697. except ValueError as e:
  698. return error_response(400, str(e))
  699. except Exception as e:
  700. return error_response(500, str(e))
  701. @router.post("/contract/approval/pass")
  702. async def contract_approval_pass(
  703. body: ContractApprovalPassBody,
  704. _user: dict = Depends(require_admin_or_super_admin),
  705. db: Session = Depends(get_db),
  706. ):
  707. try:
  708. user_id = _user.get("user_id", "") or _user.get("id", "")
  709. approve_pass(db, body.id, approval_opinion=body.approvalOpinion or "", user_id=user_id)
  710. db.commit()
  711. return success_response(None, message="审批通过")
  712. except ValueError as e:
  713. return error_response(400, str(e))
  714. except Exception as e:
  715. return error_response(500, str(e))
  716. @router.post("/contract/approval/reject")
  717. async def contract_approval_reject(
  718. body: ContractApprovalRejectBody,
  719. _user: dict = Depends(require_admin_or_super_admin),
  720. db: Session = Depends(get_db),
  721. ):
  722. try:
  723. user_id = _user.get("user_id", "") or _user.get("id", "")
  724. approve_reject(db, body.id, approval_opinion=body.approvalOpinion, user_id=user_id)
  725. db.commit()
  726. return success_response(None, message="已驳回")
  727. except ValueError as e:
  728. return error_response(400, str(e))
  729. except Exception as e:
  730. return error_response(500, str(e))
  731. @router.post("/contract/approval/transfer")
  732. async def contract_approval_transfer(
  733. body: ContractApprovalTransferBody,
  734. _user: dict = Depends(require_admin_or_super_admin),
  735. db: Session = Depends(get_db),
  736. ):
  737. try:
  738. user_id = _user.get("user_id", "") or _user.get("id", "")
  739. approve_transfer(
  740. db,
  741. body.id,
  742. target_approver_id=body.targetApproverId,
  743. transfer_reason=body.transferReason,
  744. user_id=user_id,
  745. )
  746. db.commit()
  747. return success_response(None, message="转审成功")
  748. except ValueError as e:
  749. return error_response(400, str(e))
  750. except Exception as e:
  751. return error_response(500, str(e))
  752. @router.post("/contract/approval/export")
  753. async def contract_approval_export(
  754. body: ContractApprovalExportBody = None,
  755. _user: dict = Depends(require_admin_or_super_admin),
  756. db: Session = Depends(get_db),
  757. ):
  758. try:
  759. tenant_no = get_tenant_no(db, _user)
  760. user_info = crm_user_info(_user)
  761. list_body = body or ContractApprovalExportBody()
  762. filters = list_body.model_dump(
  763. by_alias=True,
  764. exclude={"pageNum", "pageSize"},
  765. exclude_none=True,
  766. )
  767. if list_body.model_extra:
  768. filters.update(
  769. {
  770. k: v
  771. for k, v in list_body.model_extra.items()
  772. if v is not None and str(v).strip() != ""
  773. }
  774. )
  775. rows = export_contract_approvals(db, tenant_no, filters=filters)
  776. rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None) or getattr(body, "ids", None))
  777. export_cols = filter_export_columns(CONTRACT_APPROVAL_EXPORT_COLUMNS, getattr(list_body, "columns", None) or getattr(body, "columns", None))
  778. return export_rows_to_xlsx_response(
  779. db,
  780. tenant_no=tenant_no,
  781. sheet_title="合同审批记录",
  782. columns=export_cols,
  783. rows=rows,
  784. filename_stem=f"合同审批_{datetime.now().strftime('%Y%m%d%H%M%S')}",
  785. include_legacy_filename=False,
  786. )
  787. except ValueError as e:
  788. return error_response(400, str(e))
  789. except Exception as e:
  790. return error_response(500, str(e))
  791. @router.post("/contract/approval/refreshNames")
  792. async def contract_approval_refresh_names(
  793. body: ContractApprovalRefreshNamesBody = None,
  794. _user: dict = Depends(require_admin_or_super_admin),
  795. db: Session = Depends(get_db),
  796. ):
  797. try:
  798. req = body or ContractApprovalRefreshNamesBody()
  799. count = refresh_approver_names(db, contract_id=req.contractId)
  800. db.commit()
  801. return success_response(count)
  802. except ValueError as e:
  803. return error_response(400, str(e))
  804. except Exception as e:
  805. return error_response(500, str(e))