crm_quote_routes.py 27 KB

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