crm_contract_routes.py 34 KB

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