crm_contract_routes.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. """CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from typing import Optional
  5. from fastapi import Depends, Query
  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, require_menu_code
  9. from app.core.oper_log_decorator import oper_log
  10. from app.core.response import error_response, success_response
  11. from app.core.tenant_context import get_tenant_no
  12. from app.crud.crm.data_scope_helpers import crm_user_info
  13. from app.crud.crm.contract import (
  14. batch_assign_owner,
  15. batch_submit_contract_approval,
  16. create_contract,
  17. create_from_opportunity,
  18. create_from_quote,
  19. download_contract_pdf,
  20. export_contracts,
  21. finish_contract,
  22. get_contract_detail,
  23. get_template,
  24. invalid_contract,
  25. list_contracts,
  26. query_expiring,
  27. renew_contract,
  28. save_template,
  29. sign_contract,
  30. soft_delete_contracts,
  31. start_execute,
  32. statistics_by_status,
  33. statistics_by_type,
  34. statistics_overview,
  35. statistics_trend,
  36. submit_contract_approval,
  37. total_pages,
  38. update_contract,
  39. upload_attachments,
  40. upload_attachments_with_category,
  41. upload_file,
  42. withdraw_contract_approval,
  43. )
  44. from app.crud.crm.contract_approval import (
  45. approve_contract_by_contract_id,
  46. list_contract_approvals,
  47. )
  48. from app.crud.crm.contract_approver_setting import get_setting as get_contract_approver_setting
  49. from app.crud.crm.contract_approver_setting import save_setting as save_contract_approver_setting
  50. from app.schemas.crm.contract_approver_setting import ContractApproverSettingSaveBody
  51. from app.schemas.crm.contract import (
  52. ContractApprovalActionBody,
  53. ContractBatchAssignBody,
  54. ContractBatchSubmitBody,
  55. ContractCreateBody,
  56. ContractExportBody,
  57. ContractInvalidBody,
  58. ContractListQuery,
  59. ContractRenewBody,
  60. ContractSignBody,
  61. ContractStatisticsOverviewQuery,
  62. ContractStatisticsTrendQuery,
  63. ContractStatusBody,
  64. ContractSubmitBody,
  65. ContractTemplateSaveBody,
  66. ContractUpdateBody,
  67. ContractUploadAttachmentsBody,
  68. ContractUploadFileBody,
  69. ContractWithdrawBody,
  70. )
  71. from app.utils.crm_export_columns import CONTRACT_EXPORT_COLUMNS
  72. from app.utils.excel_export import (
  73. ExportValidationError,
  74. export_rows_to_xlsx_response,
  75. filter_export_columns,
  76. )
  77. from app.api.crm import _filter_export_rows_by_ids
  78. def register_contract_routes(router):
  79. @router.get("/contract/list")
  80. async def contract_list(
  81. pageNum: int = Query(1, ge=1),
  82. pageSize: int = Query(10, ge=1, le=500),
  83. keyword: Optional[str] = Query(
  84. None, description="即时检索:合同编号/合同产品名称/产品拼音首字母"
  85. ),
  86. contractNo: Optional[str] = None,
  87. contractName: Optional[str] = None,
  88. customerName: Optional[str] = None,
  89. customerId: Optional[str] = None,
  90. opportunityId: Optional[str] = None,
  91. quoteId: Optional[str] = None,
  92. contractType: Optional[str] = None,
  93. status: Optional[str] = None,
  94. excludeStatus: Optional[str] = None,
  95. approvalStatus: Optional[str] = None,
  96. ownerUserId: Optional[str] = None,
  97. pendingApprovalUserId: Optional[str] = None,
  98. approvedByUserId: Optional[str] = None,
  99. minAmount: Optional[float] = None,
  100. maxAmount: Optional[float] = None,
  101. signDateBegin: Optional[str] = None,
  102. signDateEnd: Optional[str] = None,
  103. beginTime: Optional[str] = None,
  104. endTime: Optional[str] = None,
  105. includeProducts: bool = Query(False, description="是否附带合同产品明细"),
  106. _user: dict = Depends(require_admin_or_super_admin),
  107. db: Session = Depends(get_db),
  108. ):
  109. try:
  110. tenant_no = get_tenant_no(db, _user)
  111. user_info = crm_user_info(_user)
  112. user_id = _user.get("user_id", "") or _user.get("id", "")
  113. query = ContractListQuery(
  114. pageNum=pageNum,
  115. pageSize=pageSize,
  116. keyword=keyword,
  117. contractNo=contractNo,
  118. contractName=contractName,
  119. customerName=customerName,
  120. customerId=customerId,
  121. opportunityId=opportunityId,
  122. quoteId=quoteId,
  123. contractType=contractType,
  124. status=status,
  125. excludeStatus=excludeStatus,
  126. approvalStatus=approvalStatus,
  127. ownerUserId=ownerUserId,
  128. pendingApprovalUserId=pendingApprovalUserId,
  129. approvedByUserId=approvedByUserId,
  130. minAmount=minAmount,
  131. maxAmount=maxAmount,
  132. signDateBegin=signDateBegin,
  133. signDateEnd=signDateEnd,
  134. beginTime=beginTime,
  135. endTime=endTime,
  136. )
  137. filters = query.model_dump(
  138. by_alias=True,
  139. exclude={"pageNum", "pageSize"},
  140. exclude_none=True,
  141. )
  142. rows, total = list_contracts(
  143. db,
  144. tenant_no,
  145. user_info=user_info,
  146. page=query.pageNum,
  147. page_size=query.pageSize,
  148. filters=filters,
  149. current_user_id=user_id,
  150. include_products=includeProducts,
  151. )
  152. tp = total_pages(total, query.pageSize)
  153. return success_response(
  154. {
  155. "list": rows,
  156. "rows": rows,
  157. "total": total,
  158. "pageNum": query.pageNum,
  159. "pageSize": query.pageSize,
  160. "totalPages": tp,
  161. }
  162. )
  163. except ValueError as e:
  164. return error_response(400, str(e))
  165. except Exception as e:
  166. return error_response(500, str(e))
  167. @router.post("/contract")
  168. @oper_log(title="CRM合同管理", business_type=1, summary="新增合同【{contractName}】")
  169. async def contract_add(
  170. body: ContractCreateBody,
  171. _user: dict = Depends(require_admin_or_super_admin),
  172. db: Session = Depends(get_db),
  173. ):
  174. try:
  175. tenant_no = get_tenant_no(db, _user)
  176. user_info = crm_user_info(_user)
  177. user_id = _user.get("user_id", "") or _user.get("id", "")
  178. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  179. detail = create_contract(
  180. db,
  181. tenant_no=tenant_no,
  182. data=body.model_dump(by_alias=True),
  183. user_id=user_id,
  184. dept_id=dept_id,
  185. )
  186. return success_response(detail["id"], message="新增成功")
  187. except ValueError as e:
  188. return error_response(400, str(e))
  189. except Exception as e:
  190. return error_response(500, str(e))
  191. @router.put("/contract")
  192. @oper_log(title="CRM合同管理", business_type=2, summary="修改合同【{contractName}】")
  193. async def contract_edit(
  194. body: ContractUpdateBody,
  195. _user: dict = Depends(require_admin_or_super_admin),
  196. db: Session = Depends(get_db),
  197. ):
  198. try:
  199. tenant_no = get_tenant_no(db, _user)
  200. user_info = crm_user_info(_user)
  201. user_id = _user.get("user_id", "") or _user.get("id", "")
  202. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  203. data = body.model_dump(by_alias=True)
  204. contract_id = data.pop("id")
  205. detail = update_contract(
  206. db,
  207. contract_id,
  208. tenant_no=tenant_no,
  209. data=data,
  210. user_id=user_id,
  211. dept_id=dept_id,
  212. user_info=user_info,
  213. )
  214. return success_response(detail, message="修改成功")
  215. except ValueError as e:
  216. return error_response(400, str(e))
  217. except Exception as e:
  218. return error_response(500, str(e))
  219. @router.delete("/contract/{ids}")
  220. @oper_log(title="CRM合同管理", business_type=3, path_param="ids", summary="删除合同")
  221. async def contract_remove(
  222. ids: str,
  223. _user: dict = Depends(require_admin_or_super_admin),
  224. db: Session = Depends(get_db),
  225. ):
  226. try:
  227. tenant_no = get_tenant_no(db, _user)
  228. user_info = crm_user_info(_user)
  229. id_list = [x.strip() for x in ids.split(",") if x.strip()]
  230. soft_delete_contracts(db, id_list, tenant_no=tenant_no, user_info=user_info)
  231. return success_response(None, message="删除成功")
  232. except ValueError as e:
  233. return error_response(400, str(e))
  234. except Exception as e:
  235. return error_response(500, str(e))
  236. @router.put("/contract/submitApproval")
  237. async def contract_submit_approval(
  238. body: ContractSubmitBody,
  239. _user: dict = Depends(require_admin_or_super_admin),
  240. db: Session = Depends(get_db),
  241. ):
  242. try:
  243. tenant_no = get_tenant_no(db, _user)
  244. user_info = crm_user_info(_user)
  245. user_id = _user.get("user_id", "") or _user.get("id", "")
  246. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  247. submit_contract_approval(
  248. db,
  249. body.id,
  250. tenant_no=tenant_no,
  251. approver_ids=body.approverIds,
  252. remark=body.remark,
  253. user_id=user_id,
  254. dept_id=dept_id,
  255. user_info=user_info,
  256. )
  257. return success_response(None, message="提交成功")
  258. except ValueError as e:
  259. return error_response(400, str(e))
  260. except Exception as e:
  261. return error_response(500, str(e))
  262. @router.put("/contract/batchSubmitApproval")
  263. async def contract_batch_submit_approval(
  264. body: ContractBatchSubmitBody,
  265. _user: dict = Depends(require_admin_or_super_admin),
  266. db: Session = Depends(get_db),
  267. ):
  268. try:
  269. tenant_no = get_tenant_no(db, _user)
  270. user_info = crm_user_info(_user)
  271. user_id = _user.get("user_id", "") or _user.get("id", "")
  272. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  273. count = batch_submit_contract_approval(
  274. db,
  275. body.ids,
  276. tenant_no=tenant_no,
  277. approver_ids=body.approverIds,
  278. remark=body.remark,
  279. user_id=user_id,
  280. dept_id=dept_id,
  281. user_info=user_info,
  282. )
  283. return success_response(count, message="批量提交完成")
  284. except ValueError as e:
  285. return error_response(400, str(e))
  286. except Exception as e:
  287. return error_response(500, str(e))
  288. @router.put("/contract/withdrawApproval")
  289. async def contract_withdraw_approval(
  290. body: ContractWithdrawBody,
  291. _user: dict = Depends(require_admin_or_super_admin),
  292. db: Session = Depends(get_db),
  293. ):
  294. try:
  295. tenant_no = get_tenant_no(db, _user)
  296. user_info = crm_user_info(_user)
  297. user_id = _user.get("user_id", "") or _user.get("id", "")
  298. withdraw_contract_approval(
  299. db,
  300. body.id,
  301. tenant_no=tenant_no,
  302. remark=body.remark,
  303. user_id=user_id,
  304. user_info=user_info,
  305. )
  306. return success_response(None, message="撤回成功")
  307. except ValueError as e:
  308. return error_response(400, str(e))
  309. except Exception as e:
  310. return error_response(500, str(e))
  311. @router.put("/contract/startExecute")
  312. async def contract_start_execute(
  313. body: ContractStatusBody,
  314. _user: dict = Depends(require_admin_or_super_admin),
  315. db: Session = Depends(get_db),
  316. ):
  317. try:
  318. tenant_no = get_tenant_no(db, _user)
  319. user_info = crm_user_info(_user)
  320. user_id = _user.get("user_id", "") or _user.get("id", "")
  321. start_execute(db, body.id, tenant_no=tenant_no, remark=body.remark, user_id=user_id)
  322. return success_response(None, message="操作成功")
  323. except ValueError as e:
  324. return error_response(400, str(e))
  325. except Exception as e:
  326. return error_response(500, str(e))
  327. @router.put("/contract/finish")
  328. async def contract_finish(
  329. body: ContractStatusBody,
  330. _user: dict = Depends(require_admin_or_super_admin),
  331. db: Session = Depends(get_db),
  332. ):
  333. try:
  334. tenant_no = get_tenant_no(db, _user)
  335. user_info = crm_user_info(_user)
  336. user_id = _user.get("user_id", "") or _user.get("id", "")
  337. finish_contract(db, body.id, tenant_no=tenant_no, remark=body.remark, user_id=user_id)
  338. return success_response(None, message="操作成功")
  339. except ValueError as e:
  340. return error_response(400, str(e))
  341. except Exception as e:
  342. return error_response(500, str(e))
  343. @router.put("/contract/invalid")
  344. @oper_log(title="CRM合同管理", business_type=2, summary="作废合同")
  345. async def contract_invalid(
  346. body: ContractInvalidBody,
  347. _user: dict = Depends(require_admin_or_super_admin),
  348. db: Session = Depends(get_db),
  349. ):
  350. try:
  351. tenant_no = get_tenant_no(db, _user)
  352. user_info = crm_user_info(_user)
  353. user_id = _user.get("user_id", "") or _user.get("id", "")
  354. invalid_contract(
  355. db,
  356. body.id,
  357. tenant_no=tenant_no,
  358. invalid_reason=body.invalidReason,
  359. remark=body.remark,
  360. user_id=user_id,
  361. user_info=user_info,
  362. )
  363. return success_response(None, message="作废成功")
  364. except ValueError as e:
  365. return error_response(400, str(e))
  366. except Exception as e:
  367. return error_response(500, str(e))
  368. @router.post("/contract/saveTemplate")
  369. async def contract_save_template(
  370. body: ContractTemplateSaveBody,
  371. _user: dict = Depends(require_admin_or_super_admin),
  372. db: Session = Depends(get_db),
  373. ):
  374. try:
  375. tenant_no = get_tenant_no(db, _user)
  376. user_info = crm_user_info(_user)
  377. save_template(
  378. db,
  379. body.id,
  380. body.templateContent,
  381. tenant_no=tenant_no,
  382. settlement_clause_drafts=body.settlementClauseDrafts,
  383. user_info=user_info,
  384. )
  385. return success_response(None, message="保存成功")
  386. except ValueError as e:
  387. return error_response(400, str(e))
  388. except Exception as e:
  389. return error_response(500, str(e))
  390. @router.get("/contract/getTemplate/{contract_id}")
  391. async def contract_get_template(
  392. contract_id: str,
  393. _user: dict = Depends(require_admin_or_super_admin),
  394. db: Session = Depends(get_db),
  395. ):
  396. try:
  397. tenant_no = get_tenant_no(db, _user)
  398. user_info = crm_user_info(_user)
  399. content = get_template(db, contract_id, tenant_no=tenant_no, user_info=user_info)
  400. return success_response(content or "", message="获取模板成功")
  401. except ValueError as e:
  402. return error_response(400, str(e))
  403. except Exception as e:
  404. return error_response(500, str(e))
  405. @router.post("/contract/uploadFile")
  406. async def contract_upload_file(
  407. body: ContractUploadFileBody,
  408. _user: dict = Depends(require_admin_or_super_admin),
  409. db: Session = Depends(get_db),
  410. ):
  411. try:
  412. tenant_no = get_tenant_no(db, _user)
  413. user_info = crm_user_info(_user)
  414. upload_file(db, body.id, body.fileUrl, tenant_no=tenant_no)
  415. return success_response(None, message="上传成功")
  416. except ValueError as e:
  417. return error_response(400, str(e))
  418. except Exception as e:
  419. return error_response(500, str(e))
  420. @router.post("/contract/uploadAttachments")
  421. async def contract_upload_attachments(
  422. body: ContractUploadAttachmentsBody,
  423. _user: dict = Depends(require_admin_or_super_admin),
  424. db: Session = Depends(get_db),
  425. ):
  426. try:
  427. tenant_no = get_tenant_no(db, _user)
  428. user_info = crm_user_info(_user)
  429. # attachments 显式传入(含 [])表示整表覆盖保存,允许清空扫描件
  430. if body.attachments is not None:
  431. upload_attachments_with_category(
  432. db, body.id, body.attachments, tenant_no=tenant_no,
  433. user_info=user_info,
  434. )
  435. elif body.ossIds:
  436. upload_attachments(db, body.id, body.ossIds, tenant_no=tenant_no)
  437. else:
  438. return error_response(400, "附件参数不能为空")
  439. return success_response(None, message="上传成功")
  440. except ValueError as e:
  441. return error_response(400, str(e))
  442. except Exception as e:
  443. return error_response(500, str(e))
  444. @router.post("/contract/export")
  445. @oper_log(title="CRM合同管理", business_type=5, summary="导出合同列表")
  446. async def contract_export(
  447. body: ContractExportBody = None,
  448. _user: dict = Depends(require_admin_or_super_admin),
  449. db: Session = Depends(get_db),
  450. ):
  451. try:
  452. tenant_no = get_tenant_no(db, _user)
  453. user_info = crm_user_info(_user)
  454. list_body = body or ContractExportBody()
  455. filters = list_body.model_dump(
  456. by_alias=True,
  457. exclude={"pageNum", "pageSize"},
  458. exclude_none=True,
  459. )
  460. rows = export_contracts(db, tenant_no, filters=filters)
  461. rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None))
  462. export_cols = filter_export_columns(CONTRACT_EXPORT_COLUMNS, getattr(list_body, "columns", None))
  463. return export_rows_to_xlsx_response(
  464. db,
  465. tenant_no=tenant_no,
  466. sheet_title="合同",
  467. columns=export_cols,
  468. rows=rows,
  469. filename_stem=f"合同_{datetime.now().strftime('%Y%m%d%H%M%S')}",
  470. include_legacy_filename=False,
  471. )
  472. except ExportValidationError as e:
  473. return error_response(400, str(e))
  474. except ValueError as e:
  475. return error_response(400, str(e))
  476. except Exception as e:
  477. return error_response(500, str(e))
  478. @router.get("/contract/statistics/overview")
  479. async def contract_statistics_overview(
  480. ownerUserId: Optional[str] = None,
  481. beginTime: Optional[str] = None,
  482. endTime: Optional[str] = None,
  483. _user: dict = Depends(require_admin_or_super_admin),
  484. db: Session = Depends(get_db),
  485. ):
  486. try:
  487. tenant_no = get_tenant_no(db, _user)
  488. user_info = crm_user_info(_user)
  489. req = ContractStatisticsOverviewQuery(
  490. ownerUserId=ownerUserId,
  491. beginTime=beginTime,
  492. endTime=endTime,
  493. )
  494. data = statistics_overview(
  495. db,
  496. tenant_no,
  497. owner_user_id=req.ownerUserId,
  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.get("/contract/statistics/status")
  508. async def contract_statistics_status(
  509. _user: dict = Depends(require_admin_or_super_admin),
  510. db: Session = Depends(get_db),
  511. ):
  512. try:
  513. tenant_no = get_tenant_no(db, _user)
  514. user_info = crm_user_info(_user)
  515. return success_response(statistics_by_status(db, tenant_no))
  516. except ValueError as e:
  517. return error_response(400, str(e))
  518. except Exception as e:
  519. return error_response(500, str(e))
  520. @router.get("/contract/statistics/type")
  521. async def contract_statistics_type(
  522. _user: dict = Depends(require_admin_or_super_admin),
  523. db: Session = Depends(get_db),
  524. ):
  525. try:
  526. tenant_no = get_tenant_no(db, _user)
  527. user_info = crm_user_info(_user)
  528. return success_response(statistics_by_type(db, tenant_no))
  529. except ValueError as e:
  530. return error_response(400, str(e))
  531. except Exception as e:
  532. return error_response(500, str(e))
  533. @router.get("/contract/statistics/trend")
  534. async def contract_statistics_trend(
  535. type: str = Query("month"),
  536. beginTime: Optional[str] = None,
  537. endTime: Optional[str] = None,
  538. _user: dict = Depends(require_admin_or_super_admin),
  539. db: Session = Depends(get_db),
  540. ):
  541. try:
  542. tenant_no = get_tenant_no(db, _user)
  543. user_info = crm_user_info(_user)
  544. req = ContractStatisticsTrendQuery(type=type, beginTime=beginTime, endTime=endTime)
  545. data = statistics_trend(
  546. db,
  547. tenant_no,
  548. period_type=req.type or "month",
  549. begin_time=req.beginTime,
  550. end_time=req.endTime,
  551. user_info=user_info,
  552. )
  553. return success_response(data)
  554. except ValueError as e:
  555. return error_response(400, str(e))
  556. except Exception as e:
  557. return error_response(500, str(e))
  558. @router.get("/contract/statistics/expiring")
  559. async def contract_statistics_expiring(
  560. days: int = Query(30, ge=1),
  561. _user: dict = Depends(require_admin_or_super_admin),
  562. db: Session = Depends(get_db),
  563. ):
  564. try:
  565. tenant_no = get_tenant_no(db, _user)
  566. user_info = crm_user_info(_user)
  567. rows = query_expiring(db, tenant_no, days=days)
  568. return success_response(rows)
  569. except ValueError as e:
  570. return error_response(400, str(e))
  571. except Exception as e:
  572. return error_response(500, str(e))
  573. @router.post("/contract/renew/{parent_id}")
  574. async def contract_renew(
  575. parent_id: str,
  576. body: ContractRenewBody,
  577. _user: dict = Depends(require_admin_or_super_admin),
  578. db: Session = Depends(get_db),
  579. ):
  580. try:
  581. tenant_no = get_tenant_no(db, _user)
  582. user_info = crm_user_info(_user)
  583. user_id = _user.get("user_id", "") or _user.get("id", "")
  584. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  585. new_id = renew_contract(
  586. db,
  587. parent_id,
  588. tenant_no=tenant_no,
  589. data=body.model_dump(by_alias=True),
  590. user_id=user_id,
  591. dept_id=dept_id,
  592. )
  593. return success_response(new_id, message="续签成功")
  594. except ValueError as e:
  595. return error_response(400, str(e))
  596. except Exception as e:
  597. return error_response(500, str(e))
  598. @router.post("/contract/createFromQuote/{quote_id}")
  599. async def contract_create_from_quote(
  600. quote_id: str,
  601. body: ContractCreateBody,
  602. _user: dict = Depends(require_admin_or_super_admin),
  603. db: Session = Depends(get_db),
  604. ):
  605. try:
  606. tenant_no = get_tenant_no(db, _user)
  607. user_info = crm_user_info(_user)
  608. user_id = _user.get("user_id", "") or _user.get("id", "")
  609. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  610. contract_id = create_from_quote(
  611. db,
  612. quote_id,
  613. tenant_no=tenant_no,
  614. data=body.model_dump(by_alias=True),
  615. user_id=user_id,
  616. dept_id=dept_id,
  617. )
  618. return success_response(contract_id, message="创建成功")
  619. except ValueError as e:
  620. return error_response(400, str(e))
  621. except Exception as e:
  622. return error_response(500, str(e))
  623. @router.post("/contract/createFromOpportunity/{opportunity_id}")
  624. async def contract_create_from_opportunity(
  625. opportunity_id: str,
  626. body: ContractCreateBody,
  627. _user: dict = Depends(require_admin_or_super_admin),
  628. db: Session = Depends(get_db),
  629. ):
  630. try:
  631. tenant_no = get_tenant_no(db, _user)
  632. user_info = crm_user_info(_user)
  633. user_id = _user.get("user_id", "") or _user.get("id", "")
  634. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  635. contract_id = create_from_opportunity(
  636. db,
  637. opportunity_id,
  638. tenant_no=tenant_no,
  639. data=body.model_dump(by_alias=True),
  640. user_id=user_id,
  641. dept_id=dept_id,
  642. )
  643. return success_response(contract_id, message="创建成功")
  644. except ValueError as e:
  645. return error_response(400, str(e))
  646. except Exception as e:
  647. return error_response(500, str(e))
  648. @router.put("/contract/sign")
  649. async def contract_sign(
  650. body: ContractSignBody,
  651. _user: dict = Depends(require_admin_or_super_admin),
  652. db: Session = Depends(get_db),
  653. ):
  654. try:
  655. tenant_no = get_tenant_no(db, _user)
  656. user_info = crm_user_info(_user)
  657. user_id = _user.get("user_id", "") or _user.get("id", "")
  658. sign_contract(
  659. db,
  660. body.id,
  661. tenant_no=tenant_no,
  662. actual_sign_date=body.actualSignDate,
  663. contract_file=body.contractFile,
  664. remark=body.remark or "",
  665. user_id=user_id,
  666. user_info=user_info,
  667. )
  668. return success_response(None, message="签订成功")
  669. except ValueError as e:
  670. return error_response(400, str(e))
  671. except Exception as e:
  672. return error_response(500, str(e))
  673. @router.put("/contract/batch/assign")
  674. async def contract_batch_assign(
  675. body: ContractBatchAssignBody,
  676. _user: dict = Depends(require_admin_or_super_admin),
  677. db: Session = Depends(get_db),
  678. ):
  679. try:
  680. tenant_no = get_tenant_no(db, _user)
  681. user_info = crm_user_info(_user)
  682. user_id = _user.get("user_id", "") or _user.get("id", "")
  683. count = batch_assign_owner(
  684. db,
  685. body.ids,
  686. tenant_no=tenant_no,
  687. owner_user_id=body.ownerUserId,
  688. remark=body.remark or "",
  689. user_id=user_id,
  690. user_info=user_info,
  691. )
  692. return success_response(count, message="分配成功")
  693. except ValueError as e:
  694. return error_response(400, str(e))
  695. except Exception as e:
  696. return error_response(500, str(e))
  697. @router.get("/contract/download/{contract_id}")
  698. async def contract_download(
  699. contract_id: str,
  700. _user: dict = Depends(require_admin_or_super_admin),
  701. db: Session = Depends(get_db),
  702. ):
  703. try:
  704. tenant_no = get_tenant_no(db, _user)
  705. user_info = crm_user_info(_user)
  706. file_path = download_contract_pdf(db, contract_id, tenant_no=tenant_no)
  707. return success_response(file_path)
  708. except ValueError as e:
  709. return error_response(400, str(e))
  710. except Exception as e:
  711. return error_response(500, str(e))
  712. @router.post("/contract/approval")
  713. async def contract_approval_by_contract_id(
  714. body: ContractApprovalActionBody,
  715. _user: dict = Depends(require_admin_or_super_admin),
  716. db: Session = Depends(get_db),
  717. ):
  718. try:
  719. tenant_no = get_tenant_no(db, _user)
  720. user_id = _user.get("user_id", "") or _user.get("id", "")
  721. approve_contract_by_contract_id(
  722. db,
  723. body.id,
  724. approval_result=body.approvalResult,
  725. approval_opinion=body.approvalOpinion or "",
  726. attachments=body.attachments or "",
  727. remark=body.remark or "",
  728. user_id=user_id,
  729. )
  730. db.commit()
  731. return success_response(None, message="审批成功")
  732. except ValueError as e:
  733. return error_response(400, str(e))
  734. except Exception as e:
  735. return error_response(500, str(e))
  736. @router.get("/contract/approval/flow/{contract_id}")
  737. async def contract_approval_flow(
  738. contract_id: str,
  739. _user: dict = Depends(require_admin_or_super_admin),
  740. db: Session = Depends(get_db),
  741. ):
  742. try:
  743. tenant_no = get_tenant_no(db, _user)
  744. user_info = crm_user_info(_user)
  745. rows = list_contract_approvals(db, contract_id, tenant_no=tenant_no)
  746. return success_response(rows)
  747. except ValueError as e:
  748. return error_response(400, str(e))
  749. except Exception as e:
  750. return error_response(500, str(e))
  751. @router.post("/contract-approver-setting/get")
  752. async def contract_approver_setting_get(
  753. _user: dict = Depends(require_admin_or_super_admin),
  754. db: Session = Depends(get_db),
  755. ):
  756. try:
  757. tenant_no = get_tenant_no(db, _user)
  758. data = get_contract_approver_setting(db, tenant_no=tenant_no)
  759. return success_response(data or {"approverIds": [], "status": "0"})
  760. except Exception as e:
  761. return error_response(500, str(e))
  762. @router.post("/contract-approver-setting/save")
  763. async def contract_approver_setting_save(
  764. body: ContractApproverSettingSaveBody,
  765. _user: dict = Depends(require_menu_code("system:approverSetting")),
  766. db: Session = Depends(get_db),
  767. ):
  768. try:
  769. tenant_no = get_tenant_no(db, _user)
  770. user_id = _user.get("user_id", "") or _user.get("id", "")
  771. dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
  772. data = save_contract_approver_setting(
  773. db,
  774. tenant_no=tenant_no,
  775. approver_ids=body.approverIds or [],
  776. status=body.status or "0",
  777. user_id=user_id,
  778. dept_id=dept_id,
  779. )
  780. return success_response(data, message="保存成功")
  781. except ValueError as e:
  782. return error_response(400, str(e))
  783. except Exception as e:
  784. return error_response(500, str(e))
  785. @router.get("/contract/{contract_id}")
  786. async def contract_detail(
  787. contract_id: str,
  788. _user: dict = Depends(require_admin_or_super_admin),
  789. db: Session = Depends(get_db),
  790. ):
  791. try:
  792. tenant_no = get_tenant_no(db, _user)
  793. user_info = crm_user_info(_user)
  794. user_id = _user.get("user_id", "") or _user.get("id", "")
  795. detail = get_contract_detail(
  796. db,
  797. contract_id,
  798. tenant_no=tenant_no,
  799. current_user_id=user_id,
  800. user_info=user_info,
  801. )
  802. return success_response(detail)
  803. except ValueError as e:
  804. return error_response(400, str(e))
  805. except Exception as e:
  806. return error_response(500, str(e))