crm_quote_routes.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. """CRM 报价单 API 路由 — 挂接到 crm.router"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from fastapi import Depends
  5. from sqlalchemy.orm import Session
  6. from app.core.database import get_db
  7. from app.core.menu_guard import require_admin_or_super_admin, require_menu_code
  8. from app.core.response import error_response, success_response
  9. from app.core.tenant_context import get_tenant_no
  10. from app.crud.crm.data_scope_helpers import crm_user_info
  11. from app.crud.crm.quote import (
  12. accept_quote,
  13. create_new_version,
  14. create_quote,
  15. generate_pdf,
  16. generate_share,
  17. get_quote_detail,
  18. get_template,
  19. invalid_quote,
  20. list_quotes,
  21. query_versions,
  22. reject_quote,
  23. save_template,
  24. soft_delete_quotes,
  25. statistics_overview,
  26. statistics_status,
  27. statistics_trend,
  28. submit_quote,
  29. to_contract,
  30. total_pages as quote_total_pages,
  31. update_quote,
  32. )
  33. from app.crud.crm.quote_approver_setting import get_setting, save_setting
  34. from app.crud.crm.quote_approval import (
  35. approve_quote,
  36. list_quote_approvals,
  37. refresh_approver_names,
  38. withdraw_approval,
  39. )
  40. from app.crud.crm.quote_product import (
  41. insert_quote_product,
  42. list_quote_products,
  43. soft_delete_quote_products,
  44. update_quote_product,
  45. )
  46. from app.crud.crm.quote_share_log import list_share_logs, share_log_statistics
  47. from app.schemas.crm.quote_approver_setting import QuoteApproverSettingSaveBody
  48. from app.schemas.crm.quote import (
  49. QuoteAcceptBody,
  50. QuoteApprovalApproveBody,
  51. QuoteApprovalListBody,
  52. QuoteApprovalRefreshNamesBody,
  53. QuoteApprovalWithdrawBody,
  54. QuoteCreateBody,
  55. QuoteDeleteBody,
  56. QuoteDetailBody,
  57. QuoteInvalidBody,
  58. QuoteListBody,
  59. QuoteNewVersionBody,
  60. QuotePdfBody,
  61. QuoteProductCreateBody,
  62. QuoteProductDeleteBody,
  63. QuoteProductListBody,
  64. QuoteProductUpdateBody,
  65. QuoteRejectBody,
  66. QuoteShareGenerateBody,
  67. QuoteShareLogListBody,
  68. QuoteShareLogStatisticsBody,
  69. QuoteStatisticsOverviewBody,
  70. QuoteStatisticsTrendBody,
  71. QuoteSubmitBody,
  72. QuoteTemplateGetBody,
  73. QuoteTemplateSaveBody,
  74. QuoteToContractBody,
  75. QuoteUpdateBody,
  76. QuoteVersionsBody,
  77. )
  78. from app.utils.crm_export_columns import QUOTE_EXPORT_COLUMNS
  79. from app.utils.excel_export import export_rows_to_xlsx_response
  80. def register_quote_routes(router):
  81. @router.post("/quote/list")
  82. async def quote_list(
  83. body: QuoteListBody = None,
  84. _user: dict = Depends(require_admin_or_super_admin),
  85. db: Session = Depends(get_db),
  86. ):
  87. try:
  88. tenant_no = get_tenant_no(db, _user)
  89. user_info = crm_user_info(_user)
  90. list_body = body or QuoteListBody()
  91. user_id = _user.get("user_id", "") or _user.get("id", "")
  92. rows, total = list_quotes(
  93. db,
  94. tenant_no,
  95. user_info=user_info,
  96. page=list_body.page,
  97. page_size=list_body.pageSize,
  98. filters=list_body.model_dump(by_alias=True, exclude={"page", "pageSize"}),
  99. current_user_id=user_id,
  100. )
  101. tp = quote_total_pages(total, list_body.pageSize)
  102. return success_response(
  103. {
  104. "list": rows,
  105. "rows": rows,
  106. "total": total,
  107. "page": list_body.page,
  108. "pageSize": list_body.pageSize,
  109. "totalPages": tp,
  110. }
  111. )
  112. except ValueError as e:
  113. return error_response(400, str(e))
  114. except Exception as e:
  115. return error_response(500, str(e))
  116. @router.post("/quote/detail")
  117. async def quote_detail(
  118. body: QuoteDetailBody,
  119. _user: dict = Depends(require_admin_or_super_admin),
  120. db: Session = Depends(get_db),
  121. ):
  122. try:
  123. tenant_no = get_tenant_no(db, _user)
  124. user_info = crm_user_info(_user)
  125. detail = get_quote_detail(db, body.id, tenant_no=tenant_no, user_info=user_info)
  126. return success_response(detail)
  127. except ValueError as e:
  128. return error_response(400, str(e))
  129. except Exception as e:
  130. return error_response(500, str(e))
  131. @router.post("/quote/add")
  132. async def quote_add(
  133. body: QuoteCreateBody,
  134. _user: dict = Depends(require_admin_or_super_admin),
  135. db: Session = Depends(get_db),
  136. ):
  137. try:
  138. tenant_no = get_tenant_no(db, _user)
  139. user_info = crm_user_info(_user)
  140. user_id = _user.get("user_id", "") or _user.get("id", "")
  141. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  142. detail = create_quote(
  143. db,
  144. tenant_no=tenant_no,
  145. data=body.model_dump(by_alias=True),
  146. user_id=user_id,
  147. dept_id=dept_id,
  148. )
  149. return success_response(detail, message="新增成功")
  150. except ValueError as e:
  151. return error_response(400, str(e))
  152. except Exception as e:
  153. return error_response(500, str(e))
  154. @router.post("/quote/update")
  155. async def quote_update(
  156. body: QuoteUpdateBody,
  157. _user: dict = Depends(require_admin_or_super_admin),
  158. db: Session = Depends(get_db),
  159. ):
  160. try:
  161. tenant_no = get_tenant_no(db, _user)
  162. user_info = crm_user_info(_user)
  163. user_id = _user.get("user_id", "") or _user.get("id", "")
  164. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  165. data = body.model_dump(by_alias=True)
  166. quote_id = data.pop("id")
  167. detail = update_quote(
  168. db,
  169. quote_id,
  170. tenant_no=tenant_no,
  171. data=data,
  172. user_id=user_id,
  173. dept_id=dept_id,
  174. user_info=user_info,
  175. )
  176. return success_response(detail, message="修改成功")
  177. except ValueError as e:
  178. return error_response(400, str(e))
  179. except Exception as e:
  180. return error_response(500, str(e))
  181. @router.post("/quote/delete")
  182. async def quote_delete(
  183. body: QuoteDeleteBody,
  184. _user: dict = Depends(require_admin_or_super_admin),
  185. db: Session = Depends(get_db),
  186. ):
  187. try:
  188. tenant_no = get_tenant_no(db, _user)
  189. user_info = crm_user_info(_user)
  190. soft_delete_quotes(db, body.ids, tenant_no=tenant_no, user_info=user_info)
  191. return success_response(None, message="删除成功")
  192. except ValueError as e:
  193. return error_response(400, str(e))
  194. except Exception as e:
  195. return error_response(500, str(e))
  196. @router.post("/quote/submit")
  197. async def quote_submit(
  198. body: QuoteSubmitBody,
  199. _user: dict = Depends(require_admin_or_super_admin),
  200. db: Session = Depends(get_db),
  201. ):
  202. try:
  203. tenant_no = get_tenant_no(db, _user)
  204. user_info = crm_user_info(_user)
  205. submit_quote(
  206. db,
  207. body.id,
  208. tenant_no=tenant_no,
  209. approver_ids=body.approverIds,
  210. remark=body.remark,
  211. user_info=user_info,
  212. )
  213. return success_response(None, message="提交成功")
  214. except ValueError as e:
  215. return error_response(400, str(e))
  216. except Exception as e:
  217. return error_response(500, str(e))
  218. @router.post("/quote/saveTemplate")
  219. async def quote_save_template(
  220. body: QuoteTemplateSaveBody,
  221. _user: dict = Depends(require_admin_or_super_admin),
  222. db: Session = Depends(get_db),
  223. ):
  224. try:
  225. tenant_no = get_tenant_no(db, _user)
  226. user_info = crm_user_info(_user)
  227. save_template(db, body.id, body.templateContent, tenant_no=tenant_no)
  228. return success_response(None, message="保存成功")
  229. except ValueError as e:
  230. return error_response(400, str(e))
  231. except Exception as e:
  232. return error_response(500, str(e))
  233. @router.post("/quote/getTemplate")
  234. async def quote_get_template(
  235. body: QuoteTemplateGetBody,
  236. _user: dict = Depends(require_admin_or_super_admin),
  237. db: Session = Depends(get_db),
  238. ):
  239. try:
  240. tenant_no = get_tenant_no(db, _user)
  241. user_info = crm_user_info(_user)
  242. content = get_template(db, body.id, tenant_no=tenant_no, user_info=user_info)
  243. return success_response(content or "")
  244. except ValueError as e:
  245. return error_response(400, str(e))
  246. except Exception as e:
  247. return error_response(500, str(e))
  248. @router.post("/quote/generatePdf")
  249. async def quote_generate_pdf(
  250. body: QuotePdfBody,
  251. _user: dict = Depends(require_admin_or_super_admin),
  252. db: Session = Depends(get_db),
  253. ):
  254. try:
  255. tenant_no = get_tenant_no(db, _user)
  256. user_info = crm_user_info(_user)
  257. pdf_url = generate_pdf(db, body.id, tenant_no=tenant_no)
  258. return success_response(pdf_url)
  259. except ValueError as e:
  260. return error_response(400, str(e))
  261. except Exception as e:
  262. return error_response(500, str(e))
  263. @router.post("/quote/generateShare")
  264. async def quote_generate_share(
  265. body: QuoteShareGenerateBody,
  266. _user: dict = Depends(require_admin_or_super_admin),
  267. db: Session = Depends(get_db),
  268. ):
  269. try:
  270. tenant_no = get_tenant_no(db, _user)
  271. user_info = crm_user_info(_user)
  272. link = generate_share(
  273. db,
  274. body.id,
  275. tenant_no=tenant_no,
  276. validity_days=body.validityDays,
  277. password=body.password or "",
  278. user_info=user_info,
  279. )
  280. return success_response(link)
  281. except ValueError as e:
  282. return error_response(400, str(e))
  283. except Exception as e:
  284. return error_response(500, str(e))
  285. @router.post("/quote/accept")
  286. async def quote_accept(
  287. body: QuoteAcceptBody,
  288. _user: dict = Depends(require_admin_or_super_admin),
  289. db: Session = Depends(get_db),
  290. ):
  291. try:
  292. tenant_no = get_tenant_no(db, _user)
  293. user_info = crm_user_info(_user)
  294. accept_quote(db, body.id, tenant_no=tenant_no, remark=body.remark)
  295. return success_response(None, message="操作成功")
  296. except ValueError as e:
  297. return error_response(400, str(e))
  298. except Exception as e:
  299. return error_response(500, str(e))
  300. @router.post("/quote/reject")
  301. async def quote_reject(
  302. body: QuoteRejectBody,
  303. _user: dict = Depends(require_admin_or_super_admin),
  304. db: Session = Depends(get_db),
  305. ):
  306. try:
  307. tenant_no = get_tenant_no(db, _user)
  308. user_info = crm_user_info(_user)
  309. reject_quote(
  310. db,
  311. body.id,
  312. tenant_no=tenant_no,
  313. reject_reason=body.rejectReason,
  314. remark=body.remark,
  315. user_info=user_info,
  316. )
  317. return success_response(None, message="操作成功")
  318. except ValueError as e:
  319. return error_response(400, str(e))
  320. except Exception as e:
  321. return error_response(500, str(e))
  322. @router.post("/quote/toContract")
  323. async def quote_to_contract(
  324. body: QuoteToContractBody,
  325. _user: dict = Depends(require_admin_or_super_admin),
  326. db: Session = Depends(get_db),
  327. ):
  328. try:
  329. tenant_no = get_tenant_no(db, _user)
  330. user_info = crm_user_info(_user)
  331. user_id = _user.get("user_id", "") or _user.get("id", "")
  332. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  333. contract_id = to_contract(
  334. db,
  335. body.id,
  336. tenant_no=tenant_no,
  337. contract_no=body.contractNo,
  338. contract_title=body.contractTitle,
  339. sign_date=body.signDate,
  340. start_date=body.startDate,
  341. end_date=body.endDate,
  342. user_id=user_id,
  343. dept_id=dept_id,
  344. user_info=user_info,
  345. )
  346. return success_response(contract_id)
  347. except ValueError as e:
  348. return error_response(400, str(e))
  349. except Exception as e:
  350. return error_response(500, str(e))
  351. @router.post("/quote/newVersion")
  352. async def quote_new_version(
  353. body: QuoteNewVersionBody,
  354. _user: dict = Depends(require_admin_or_super_admin),
  355. db: Session = Depends(get_db),
  356. ):
  357. try:
  358. tenant_no = get_tenant_no(db, _user)
  359. user_info = crm_user_info(_user)
  360. user_id = _user.get("user_id", "") or _user.get("id", "")
  361. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  362. new_id = create_new_version(
  363. db,
  364. body.id,
  365. tenant_no=tenant_no,
  366. version=body.version,
  367. remark=body.remark,
  368. user_id=user_id,
  369. dept_id=dept_id,
  370. user_info=user_info,
  371. )
  372. return success_response(new_id)
  373. except ValueError as e:
  374. return error_response(400, str(e))
  375. except Exception as e:
  376. return error_response(500, str(e))
  377. @router.post("/quote/versions")
  378. async def quote_versions(
  379. body: QuoteVersionsBody,
  380. _user: dict = Depends(require_admin_or_super_admin),
  381. db: Session = Depends(get_db),
  382. ):
  383. try:
  384. tenant_no = get_tenant_no(db, _user)
  385. user_info = crm_user_info(_user)
  386. rows = query_versions(db, body.quoteNo, tenant_no=tenant_no)
  387. return success_response(rows)
  388. except ValueError as e:
  389. return error_response(400, str(e))
  390. except Exception as e:
  391. return error_response(500, str(e))
  392. @router.post("/quote/invalid")
  393. async def quote_invalid(
  394. body: QuoteInvalidBody,
  395. _user: dict = Depends(require_admin_or_super_admin),
  396. db: Session = Depends(get_db),
  397. ):
  398. try:
  399. tenant_no = get_tenant_no(db, _user)
  400. user_info = crm_user_info(_user)
  401. invalid_quote(db, body.id, tenant_no=tenant_no, remark=body.remark)
  402. return success_response(None, message="作废成功")
  403. except ValueError as e:
  404. return error_response(400, str(e))
  405. except Exception as e:
  406. return error_response(500, str(e))
  407. @router.post("/quote/export")
  408. async def quote_export(
  409. body: QuoteListBody = None,
  410. _user: dict = Depends(require_admin_or_super_admin),
  411. db: Session = Depends(get_db),
  412. ):
  413. try:
  414. tenant_no = get_tenant_no(db, _user)
  415. user_info = crm_user_info(_user)
  416. list_body = body or QuoteListBody()
  417. rows, _ = list_quotes(
  418. db,
  419. tenant_no,
  420. user_info=user_info,
  421. page=1,
  422. page_size=10000,
  423. filters=list_body.model_dump(by_alias=True, exclude={"page", "pageSize"}),
  424. )
  425. return export_rows_to_xlsx_response(
  426. db,
  427. tenant_no=tenant_no,
  428. sheet_title="报价单",
  429. columns=QUOTE_EXPORT_COLUMNS,
  430. rows=rows,
  431. filename_stem=f"报价单_{datetime.now().strftime('%Y%m%d%H%M%S')}",
  432. include_legacy_filename=False,
  433. )
  434. except ValueError as e:
  435. return error_response(400, str(e))
  436. except Exception as e:
  437. return error_response(500, str(e))
  438. @router.post("/quote/statistics/overview")
  439. async def quote_statistics_overview(
  440. body: QuoteStatisticsOverviewBody = None,
  441. _user: dict = Depends(require_admin_or_super_admin),
  442. db: Session = Depends(get_db),
  443. ):
  444. try:
  445. tenant_no = get_tenant_no(db, _user)
  446. user_info = crm_user_info(_user)
  447. req = body or QuoteStatisticsOverviewBody()
  448. data = statistics_overview(
  449. db,
  450. tenant_no,
  451. owner_user_id=req.ownerUserId,
  452. begin_time=req.beginTime,
  453. end_time=req.endTime,
  454. user_info=user_info,
  455. )
  456. return success_response(data)
  457. except ValueError as e:
  458. return error_response(400, str(e))
  459. except Exception as e:
  460. return error_response(500, str(e))
  461. @router.post("/quote/statistics/status")
  462. async def quote_statistics_status(
  463. _user: dict = Depends(require_admin_or_super_admin),
  464. db: Session = Depends(get_db),
  465. ):
  466. try:
  467. tenant_no = get_tenant_no(db, _user)
  468. user_info = crm_user_info(_user)
  469. return success_response(statistics_status(db, tenant_no))
  470. except ValueError as e:
  471. return error_response(400, str(e))
  472. except Exception as e:
  473. return error_response(500, str(e))
  474. @router.post("/quote/statistics/trend")
  475. async def quote_statistics_trend(
  476. body: QuoteStatisticsTrendBody = None,
  477. _user: dict = Depends(require_admin_or_super_admin),
  478. db: Session = Depends(get_db),
  479. ):
  480. try:
  481. tenant_no = get_tenant_no(db, _user)
  482. user_info = crm_user_info(_user)
  483. req = body or QuoteStatisticsTrendBody()
  484. data = statistics_trend(
  485. db,
  486. tenant_no,
  487. period_type=req.type or "month",
  488. begin_time=req.beginTime,
  489. end_time=req.endTime,
  490. user_info=user_info,
  491. )
  492. return success_response(data)
  493. except ValueError as e:
  494. return error_response(400, str(e))
  495. except Exception as e:
  496. return error_response(500, str(e))
  497. @router.post("/quote/product/list")
  498. async def quote_product_list(
  499. body: QuoteProductListBody,
  500. _user: dict = Depends(require_admin_or_super_admin),
  501. db: Session = Depends(get_db),
  502. ):
  503. try:
  504. tenant_no = get_tenant_no(db, _user)
  505. user_info = crm_user_info(_user)
  506. rows = list_quote_products(db, body.quoteId, tenant_no=tenant_no)
  507. return success_response(rows)
  508. except ValueError as e:
  509. return error_response(400, str(e))
  510. except Exception as e:
  511. return error_response(500, str(e))
  512. @router.post("/quote/product/add")
  513. async def quote_product_add(
  514. body: QuoteProductCreateBody,
  515. _user: dict = Depends(require_admin_or_super_admin),
  516. db: Session = Depends(get_db),
  517. ):
  518. try:
  519. tenant_no = get_tenant_no(db, _user)
  520. user_info = crm_user_info(_user)
  521. user_id = _user.get("user_id", "") or _user.get("id", "")
  522. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  523. obj = insert_quote_product(
  524. db,
  525. tenant_no=tenant_no,
  526. data=body.model_dump(by_alias=True),
  527. user_id=user_id,
  528. dept_id=dept_id,
  529. )
  530. from app.crud.crm.quote_product import product_row_to_dict
  531. return success_response(product_row_to_dict(obj), 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.post("/quote/product/update")
  537. async def quote_product_update(
  538. body: QuoteProductUpdateBody,
  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. data = body.model_dump(by_alias=True)
  547. line_id = data.pop("id")
  548. obj = update_quote_product(
  549. db,
  550. line_id,
  551. tenant_no=tenant_no,
  552. data=data,
  553. user_id=user_id,
  554. user_info=user_info,
  555. )
  556. from app.crud.crm.quote_product import product_row_to_dict
  557. return success_response(product_row_to_dict(obj), message="修改成功")
  558. except ValueError as e:
  559. return error_response(400, str(e))
  560. except Exception as e:
  561. return error_response(500, str(e))
  562. @router.post("/quote/product/delete")
  563. async def quote_product_delete(
  564. body: QuoteProductDeleteBody,
  565. _user: dict = Depends(require_admin_or_super_admin),
  566. db: Session = Depends(get_db),
  567. ):
  568. try:
  569. tenant_no = get_tenant_no(db, _user)
  570. user_info = crm_user_info(_user)
  571. soft_delete_quote_products(db, body.ids, tenant_no=tenant_no, user_info=user_info)
  572. return success_response(None, message="删除成功")
  573. except ValueError as e:
  574. return error_response(400, str(e))
  575. except Exception as e:
  576. return error_response(500, str(e))
  577. @router.post("/quote/approval/list")
  578. async def quote_approval_list(
  579. body: QuoteApprovalListBody,
  580. _user: dict = Depends(require_admin_or_super_admin),
  581. db: Session = Depends(get_db),
  582. ):
  583. try:
  584. tenant_no = get_tenant_no(db, _user)
  585. user_info = crm_user_info(_user)
  586. rows = list_quote_approvals(db, body.quoteId, tenant_no=tenant_no)
  587. return success_response(rows)
  588. except ValueError as e:
  589. return error_response(400, str(e))
  590. except Exception as e:
  591. return error_response(500, str(e))
  592. @router.post("/quote/approval/approve")
  593. async def quote_approval_approve(
  594. body: QuoteApprovalApproveBody,
  595. _user: dict = Depends(require_admin_or_super_admin),
  596. db: Session = Depends(get_db),
  597. ):
  598. try:
  599. tenant_no = get_tenant_no(db, _user)
  600. user_id = _user.get("user_id", "") or _user.get("id", "")
  601. approve_quote(
  602. db,
  603. body.id,
  604. tenant_no=tenant_no,
  605. approval_result=body.approvalResult,
  606. approval_opinion=body.approvalOpinion or "",
  607. remark=body.remark or "",
  608. user_id=user_id,
  609. )
  610. return success_response(None, message="审批成功")
  611. except ValueError as e:
  612. return error_response(400, str(e))
  613. except Exception as e:
  614. return error_response(500, str(e))
  615. @router.post("/quote/approval/withdraw")
  616. async def quote_approval_withdraw(
  617. body: QuoteApprovalWithdrawBody,
  618. _user: dict = Depends(require_admin_or_super_admin),
  619. db: Session = Depends(get_db),
  620. ):
  621. try:
  622. tenant_no = get_tenant_no(db, _user)
  623. user_id = _user.get("user_id", "") or _user.get("id", "")
  624. withdraw_approval(
  625. db,
  626. body.quoteId,
  627. tenant_no=tenant_no,
  628. remark=body.remark or "",
  629. user_id=user_id,
  630. )
  631. return success_response(None, message="撤回成功")
  632. except ValueError as e:
  633. return error_response(400, str(e))
  634. except Exception as e:
  635. return error_response(500, str(e))
  636. @router.post("/quote/approval/refreshNames")
  637. async def quote_approval_refresh_names(
  638. body: QuoteApprovalRefreshNamesBody = None,
  639. _user: dict = Depends(require_admin_or_super_admin),
  640. db: Session = Depends(get_db),
  641. ):
  642. try:
  643. req = body or QuoteApprovalRefreshNamesBody()
  644. count = refresh_approver_names(db, quote_id=req.quoteId)
  645. return success_response(count)
  646. except ValueError as e:
  647. return error_response(400, str(e))
  648. except Exception as e:
  649. return error_response(500, str(e))
  650. @router.post("/quote/shareLog/list")
  651. async def quote_share_log_list(
  652. body: QuoteShareLogListBody,
  653. _user: dict = Depends(require_admin_or_super_admin),
  654. db: Session = Depends(get_db),
  655. ):
  656. try:
  657. tenant_no = get_tenant_no(db, _user)
  658. user_info = crm_user_info(_user)
  659. rows = list_share_logs(db, body.quoteId, tenant_no=tenant_no)
  660. return success_response(rows)
  661. except ValueError as e:
  662. return error_response(400, str(e))
  663. except Exception as e:
  664. return error_response(500, str(e))
  665. @router.post("/quote/shareLog/statistics")
  666. async def quote_share_log_statistics(
  667. body: QuoteShareLogStatisticsBody,
  668. _user: dict = Depends(require_admin_or_super_admin),
  669. db: Session = Depends(get_db),
  670. ):
  671. try:
  672. tenant_no = get_tenant_no(db, _user)
  673. user_info = crm_user_info(_user)
  674. stats = share_log_statistics(db, body.quoteId, tenant_no=tenant_no)
  675. return success_response(stats)
  676. except ValueError as e:
  677. return error_response(400, str(e))
  678. except Exception as e:
  679. return error_response(500, str(e))
  680. @router.post("/quote-approver-setting/get")
  681. async def quote_approver_setting_get(
  682. _user: dict = Depends(require_admin_or_super_admin),
  683. db: Session = Depends(get_db),
  684. ):
  685. try:
  686. tenant_no = get_tenant_no(db, _user)
  687. data = get_setting(db, tenant_no=tenant_no)
  688. # 无配置时默认「启用」,避免设置页首次打开落到「停用」导致保存后无法预填
  689. return success_response(data or {"approverIds": [], "status": "0"})
  690. except Exception as e:
  691. return error_response(500, str(e))
  692. @router.post("/quote-approver-setting/save")
  693. async def quote_approver_setting_save(
  694. body: QuoteApproverSettingSaveBody,
  695. _user: dict = Depends(require_menu_code("system:approverSetting")),
  696. db: Session = Depends(get_db),
  697. ):
  698. try:
  699. tenant_no = get_tenant_no(db, _user)
  700. user_id = _user.get("user_id", "") or _user.get("id", "")
  701. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  702. data = save_setting(
  703. db,
  704. tenant_no=tenant_no,
  705. approver_ids=body.approverIds or [],
  706. status=body.status or "0",
  707. user_id=user_id,
  708. dept_id=dept_id,
  709. )
  710. return success_response(data, message="保存成功")
  711. except ValueError as e:
  712. return error_response(400, str(e))
  713. except Exception as e:
  714. return error_response(500, str(e))