"""CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM""" from __future__ import annotations from datetime import datetime from typing import Optional from fastapi import Depends, Query from sqlalchemy.orm import Session from app.core.database import get_db from app.core.menu_guard import require_admin_or_super_admin, require_menu_code from app.core.oper_log_decorator import oper_log from app.core.response import error_response, success_response from app.core.tenant_context import get_tenant_no from app.crud.crm.data_scope_helpers import crm_user_info from app.crud.crm.contract import ( batch_assign_owner, batch_submit_contract_approval, create_contract, create_from_opportunity, create_from_quote, download_contract_pdf, export_contracts, finish_contract, get_contract_detail, get_template, invalid_contract, list_contracts, query_expiring, renew_contract, save_template, sign_contract, soft_delete_contracts, start_execute, statistics_by_status, statistics_by_type, statistics_overview, statistics_trend, submit_contract_approval, total_pages, update_contract, upload_attachments, upload_attachments_with_category, upload_file, withdraw_contract_approval, ) from app.crud.crm.contract_approval import ( approve_contract_by_contract_id, list_contract_approvals, ) from app.crud.crm.contract_approver_setting import get_setting as get_contract_approver_setting from app.crud.crm.contract_approver_setting import save_setting as save_contract_approver_setting from app.schemas.crm.contract_approver_setting import ContractApproverSettingSaveBody from app.schemas.crm.contract import ( ContractApprovalActionBody, ContractBatchAssignBody, ContractBatchSubmitBody, ContractCreateBody, ContractExportBody, ContractInvalidBody, ContractListQuery, ContractRenewBody, ContractSignBody, ContractStatisticsOverviewQuery, ContractStatisticsTrendQuery, ContractStatusBody, ContractSubmitBody, ContractTemplateSaveBody, ContractUpdateBody, ContractUploadAttachmentsBody, ContractUploadFileBody, ContractWithdrawBody, ) from app.utils.crm_export_columns import CONTRACT_EXPORT_COLUMNS from app.utils.excel_export import ( ExportValidationError, export_rows_to_xlsx_response, filter_export_columns, ) from app.api.crm import _filter_export_rows_by_ids def register_contract_routes(router): @router.get("/contract/list") async def contract_list( pageNum: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=500), keyword: Optional[str] = Query( None, description="即时检索:合同编号/合同产品名称/产品拼音首字母" ), contractNo: Optional[str] = None, contractName: Optional[str] = None, customerName: Optional[str] = None, customerId: Optional[str] = None, opportunityId: Optional[str] = None, quoteId: Optional[str] = None, contractType: Optional[str] = None, status: Optional[str] = None, excludeStatus: Optional[str] = None, approvalStatus: Optional[str] = None, ownerUserId: Optional[str] = None, pendingApprovalUserId: Optional[str] = None, approvedByUserId: Optional[str] = None, minAmount: Optional[float] = None, maxAmount: Optional[float] = None, signDateBegin: Optional[str] = None, signDateEnd: Optional[str] = None, beginTime: Optional[str] = None, endTime: Optional[str] = None, includeProducts: bool = Query(False, description="是否附带合同产品明细"), _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") query = ContractListQuery( pageNum=pageNum, pageSize=pageSize, keyword=keyword, contractNo=contractNo, contractName=contractName, customerName=customerName, customerId=customerId, opportunityId=opportunityId, quoteId=quoteId, contractType=contractType, status=status, excludeStatus=excludeStatus, approvalStatus=approvalStatus, ownerUserId=ownerUserId, pendingApprovalUserId=pendingApprovalUserId, approvedByUserId=approvedByUserId, minAmount=minAmount, maxAmount=maxAmount, signDateBegin=signDateBegin, signDateEnd=signDateEnd, beginTime=beginTime, endTime=endTime, ) filters = query.model_dump( by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True, ) rows, total = list_contracts( db, tenant_no, user_info=user_info, page=query.pageNum, page_size=query.pageSize, filters=filters, current_user_id=user_id, include_products=includeProducts, ) tp = total_pages(total, query.pageSize) return success_response( { "list": rows, "rows": rows, "total": total, "pageNum": query.pageNum, "pageSize": query.pageSize, "totalPages": tp, } ) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract") @oper_log(title="CRM合同管理", business_type=1, summary="新增合同【{contractName}】") async def contract_add( body: ContractCreateBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" detail = create_contract( db, tenant_no=tenant_no, data=body.model_dump(by_alias=True), user_id=user_id, dept_id=dept_id, ) return success_response(detail["id"], message="新增成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.put("/contract") @oper_log(title="CRM合同管理", business_type=2, summary="修改合同【{contractName}】") async def contract_edit( body: ContractUpdateBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" data = body.model_dump(by_alias=True) contract_id = data.pop("id") detail = update_contract( db, contract_id, tenant_no=tenant_no, data=data, user_id=user_id, dept_id=dept_id, user_info=user_info, ) return success_response(detail, message="修改成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.delete("/contract/{ids}") @oper_log(title="CRM合同管理", business_type=3, path_param="ids", summary="删除合同") async def contract_remove( ids: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) id_list = [x.strip() for x in ids.split(",") if x.strip()] soft_delete_contracts(db, id_list, tenant_no=tenant_no, user_info=user_info) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.put("/contract/submitApproval") async def contract_submit_approval( body: ContractSubmitBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" submit_contract_approval( db, body.id, tenant_no=tenant_no, approver_ids=body.approverIds, remark=body.remark, user_id=user_id, dept_id=dept_id, user_info=user_info, ) return success_response(None, message="提交成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.put("/contract/batchSubmitApproval") async def contract_batch_submit_approval( body: ContractBatchSubmitBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" count = batch_submit_contract_approval( db, body.ids, tenant_no=tenant_no, approver_ids=body.approverIds, remark=body.remark, user_id=user_id, dept_id=dept_id, user_info=user_info, ) return success_response(count, message="批量提交完成") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.put("/contract/withdrawApproval") async def contract_withdraw_approval( body: ContractWithdrawBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") withdraw_contract_approval( db, body.id, tenant_no=tenant_no, remark=body.remark, user_id=user_id, user_info=user_info, ) return success_response(None, message="撤回成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.put("/contract/startExecute") async def contract_start_execute( body: ContractStatusBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") start_execute(db, body.id, tenant_no=tenant_no, remark=body.remark, user_id=user_id) return success_response(None, message="操作成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.put("/contract/finish") async def contract_finish( body: ContractStatusBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") finish_contract(db, body.id, tenant_no=tenant_no, remark=body.remark, user_id=user_id) return success_response(None, message="操作成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.put("/contract/invalid") @oper_log(title="CRM合同管理", business_type=2, summary="作废合同") async def contract_invalid( body: ContractInvalidBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") invalid_contract( db, body.id, tenant_no=tenant_no, invalid_reason=body.invalidReason, remark=body.remark, user_id=user_id, user_info=user_info, ) return success_response(None, message="作废成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/saveTemplate") async def contract_save_template( body: ContractTemplateSaveBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) save_template( db, body.id, body.templateContent, tenant_no=tenant_no, settlement_clause_drafts=body.settlementClauseDrafts, user_info=user_info, ) return success_response(None, message="保存成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/getTemplate/{contract_id}") async def contract_get_template( contract_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) content = get_template(db, contract_id, tenant_no=tenant_no, user_info=user_info) return success_response(content or "", message="获取模板成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/uploadFile") async def contract_upload_file( body: ContractUploadFileBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) upload_file(db, body.id, body.fileUrl, tenant_no=tenant_no) return success_response(None, message="上传成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/uploadAttachments") async def contract_upload_attachments( body: ContractUploadAttachmentsBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) # attachments 显式传入(含 [])表示整表覆盖保存,允许清空扫描件 if body.attachments is not None: upload_attachments_with_category( db, body.id, body.attachments, tenant_no=tenant_no, user_info=user_info, ) elif body.ossIds: upload_attachments(db, body.id, body.ossIds, tenant_no=tenant_no) else: return error_response(400, "附件参数不能为空") return success_response(None, message="上传成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/export") @oper_log(title="CRM合同管理", business_type=5, summary="导出合同列表") async def contract_export( body: ContractExportBody = None, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) list_body = body or ContractExportBody() filters = list_body.model_dump( by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True, ) rows = export_contracts(db, tenant_no, filters=filters) rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None)) export_cols = filter_export_columns(CONTRACT_EXPORT_COLUMNS, getattr(list_body, "columns", None)) return export_rows_to_xlsx_response( db, tenant_no=tenant_no, sheet_title="合同", columns=export_cols, rows=rows, filename_stem=f"合同_{datetime.now().strftime('%Y%m%d%H%M%S')}", include_legacy_filename=False, ) except ExportValidationError as e: return error_response(400, str(e)) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/statistics/overview") async def contract_statistics_overview( ownerUserId: Optional[str] = None, beginTime: Optional[str] = None, endTime: Optional[str] = None, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) req = ContractStatisticsOverviewQuery( ownerUserId=ownerUserId, beginTime=beginTime, endTime=endTime, ) data = statistics_overview( db, tenant_no, owner_user_id=req.ownerUserId, begin_time=req.beginTime, end_time=req.endTime, user_info=user_info, ) return success_response(data) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/statistics/status") async def contract_statistics_status( _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) return success_response(statistics_by_status(db, tenant_no)) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/statistics/type") async def contract_statistics_type( _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) return success_response(statistics_by_type(db, tenant_no)) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/statistics/trend") async def contract_statistics_trend( type: str = Query("month"), beginTime: Optional[str] = None, endTime: Optional[str] = None, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) req = ContractStatisticsTrendQuery(type=type, beginTime=beginTime, endTime=endTime) data = statistics_trend( db, tenant_no, period_type=req.type or "month", begin_time=req.beginTime, end_time=req.endTime, user_info=user_info, ) return success_response(data) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/statistics/expiring") async def contract_statistics_expiring( days: int = Query(30, ge=1), _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) rows = query_expiring(db, tenant_no, days=days) return success_response(rows) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/renew/{parent_id}") async def contract_renew( parent_id: str, body: ContractRenewBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" new_id = renew_contract( db, parent_id, tenant_no=tenant_no, data=body.model_dump(by_alias=True), user_id=user_id, dept_id=dept_id, ) return success_response(new_id, message="续签成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/createFromQuote/{quote_id}") async def contract_create_from_quote( quote_id: str, body: ContractCreateBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" contract_id = create_from_quote( db, quote_id, tenant_no=tenant_no, data=body.model_dump(by_alias=True), user_id=user_id, dept_id=dept_id, ) return success_response(contract_id, message="创建成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/createFromOpportunity/{opportunity_id}") async def contract_create_from_opportunity( opportunity_id: str, body: ContractCreateBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" contract_id = create_from_opportunity( db, opportunity_id, tenant_no=tenant_no, data=body.model_dump(by_alias=True), user_id=user_id, dept_id=dept_id, ) return success_response(contract_id, message="创建成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.put("/contract/sign") async def contract_sign( body: ContractSignBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") sign_contract( db, body.id, tenant_no=tenant_no, actual_sign_date=body.actualSignDate, contract_file=body.contractFile, remark=body.remark or "", user_id=user_id, user_info=user_info, ) return success_response(None, message="签订成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.put("/contract/batch/assign") async def contract_batch_assign( body: ContractBatchAssignBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") count = batch_assign_owner( db, body.ids, tenant_no=tenant_no, owner_user_id=body.ownerUserId, remark=body.remark or "", user_id=user_id, user_info=user_info, ) return success_response(count, message="分配成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/download/{contract_id}") async def contract_download( contract_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) file_path = download_contract_pdf(db, contract_id, tenant_no=tenant_no) return success_response(file_path) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/approval") async def contract_approval_by_contract_id( body: ContractApprovalActionBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_id = _user.get("user_id", "") or _user.get("id", "") approve_contract_by_contract_id( db, body.id, approval_result=body.approvalResult, approval_opinion=body.approvalOpinion or "", attachments=body.attachments or "", remark=body.remark or "", user_id=user_id, ) db.commit() return success_response(None, message="审批成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/approval/flow/{contract_id}") async def contract_approval_flow( contract_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) rows = list_contract_approvals(db, contract_id, tenant_no=tenant_no) return success_response(rows) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract-approver-setting/get") async def contract_approver_setting_get( _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) data = get_contract_approver_setting(db, tenant_no=tenant_no) return success_response(data or {"approverIds": [], "status": "0"}) except Exception as e: return error_response(500, str(e)) @router.post("/contract-approver-setting/save") async def contract_approver_setting_save( body: ContractApproverSettingSaveBody, _user: dict = Depends(require_menu_code("system:approverSetting")), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" data = save_contract_approver_setting( db, tenant_no=tenant_no, approver_ids=body.approverIds or [], status=body.status or "0", user_id=user_id, dept_id=dept_id, ) return success_response(data, message="保存成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/{contract_id}") async def contract_detail( contract_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = get_tenant_no(db, _user) user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") detail = get_contract_detail( db, contract_id, tenant_no=tenant_no, current_user_id=user_id, user_info=user_info, ) return success_response(detail) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e))