crm_contract_routes.py 32 KB

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