"""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 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 ( create_contract_change, create_contract_template, export_contract_changes, export_contract_templates, execute_change, get_contract_change_detail, get_contract_template_detail, list_contract_changes, list_contract_templates, soft_delete_contract_changes, soft_delete_contract_templates, submit_change_approval, toggle_contract_template_active, total_pages, update_contract_change, update_contract_template, withdraw_change, ) from app.crud.crm.contract_approval import ( approve_pass, approve_reject, approve_transfer, export_contract_approvals, get_contract_approval_detail, list_contract_approvals_page, refresh_approver_names, ) from app.schemas.crm.contract import ( ContractApprovalExportBody, ContractApprovalListQuery, ContractApprovalPassBody, ContractApprovalRefreshNamesBody, ContractApprovalRejectBody, ContractApprovalTransferBody, ContractChangeCreateBody, ContractChangeExportBody, ContractChangeListQuery, ContractChangeSubmitBody, ContractChangeUpdateBody, ContractTemplateCreateBody, ContractTemplateExportBody, ContractTemplateListQuery, ContractTemplateUpdateBody, ) from app.utils.crm_export_columns import ( CONTRACT_APPROVAL_EXPORT_COLUMNS, CONTRACT_CHANGE_EXPORT_COLUMNS, CONTRACT_TEMPLATE_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_sub_routes(router): # --- 合同变更 --- @router.get("/contract/change/list") async def contract_change_list( pageNum: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=500), contractId: Optional[str] = None, contractNo: Optional[str] = None, contractName: Optional[str] = None, changeType: Optional[str] = None, approvalStatus: 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) query = ContractChangeListQuery( pageNum=pageNum, pageSize=pageSize, contractId=contractId, contractNo=contractNo, contractName=contractName, changeType=changeType, approvalStatus=approvalStatus, beginTime=beginTime, endTime=endTime, ) rows, total = list_contract_changes( db, tenant_no, user_info=user_info, page=query.pageNum, page_size=query.pageSize, filters=query.model_dump(by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True), ) return success_response( { "list": rows, "rows": rows, "total": total, "pageNum": query.pageNum, "pageSize": query.pageSize, "totalPages": total_pages(total, query.pageSize), } ) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/change/{change_id}") async def contract_change_detail( change_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) detail = get_contract_change_detail(db, change_id, tenant_no=tenant_no, 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)) @router.post("/contract/change") async def contract_change_add( body: ContractChangeCreateBody, _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_change( 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/change") async def contract_change_edit( body: ContractChangeUpdateBody, _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", "") data = body.model_dump(by_alias=True) change_id = data.pop("id") detail = update_contract_change( db, change_id, tenant_no=tenant_no, data=data, user_id=user_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/change/{ids}") async def contract_change_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_contract_changes(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.post("/contract/change/submitApproval") async def contract_change_submit_approval( body: ContractChangeSubmitBody, _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", "") submit_change_approval( db, body.id, tenant_no=tenant_no, approver_ids=body.approverIds, remark=body.remark or "", 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/change/withdraw/{change_id}") async def contract_change_withdraw( change_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", "") withdraw_change(db, change_id, tenant_no=tenant_no, 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/change/execute/{change_id}") async def contract_change_execute( change_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", "") execute_change(db, change_id, tenant_no=tenant_no, 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.post("/contract/change/export") async def contract_change_export( body: ContractChangeExportBody = 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 ContractChangeExportBody() filters = list_body.model_dump( by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True, ) rows = export_contract_changes(db, tenant_no, filters=filters) rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None) or getattr(body, "ids", None)) export_cols = filter_export_columns(CONTRACT_CHANGE_EXPORT_COLUMNS, getattr(list_body, "columns", None) or getattr(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 ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) # --- 合同模板库 --- @router.get("/contract/template/list") async def contract_template_list( pageNum: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=500), templateNo: Optional[str] = None, templateName: Optional[str] = None, templateType: Optional[str] = None, isActive: Optional[int] = 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) query = ContractTemplateListQuery( pageNum=pageNum, pageSize=pageSize, templateNo=templateNo, templateName=templateName, templateType=templateType, isActive=isActive, beginTime=beginTime, endTime=endTime, ) rows, total = list_contract_templates( db, tenant_no, user_info=user_info, page=query.pageNum, page_size=query.pageSize, filters=query.model_dump(by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True), ) return success_response( { "list": rows, "rows": rows, "total": total, "pageNum": query.pageNum, "pageSize": query.pageSize, "totalPages": total_pages(total, query.pageSize), } ) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/template/{template_id}") async def contract_template_detail( template_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) detail = get_contract_template_detail(db, template_id, tenant_no=tenant_no) return success_response(detail) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/template") async def contract_template_add( body: ContractTemplateCreateBody, _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_template( 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/template") async def contract_template_edit( body: ContractTemplateUpdateBody, _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", "") data = body.model_dump(by_alias=True) template_id = data.pop("id") detail = update_contract_template( db, template_id, tenant_no=tenant_no, data=data, user_id=user_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/template/{ids}") async def contract_template_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_contract_templates(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/template/enable/{template_id}") async def contract_template_enable( template_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", "") toggle_contract_template_active( db, template_id, tenant_no=tenant_no, is_active=1, 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/template/disable/{template_id}") async def contract_template_disable( template_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", "") toggle_contract_template_active( db, template_id, tenant_no=tenant_no, is_active=0, 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.post("/contract/template/export") async def contract_template_export( body: ContractTemplateExportBody = 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 ContractTemplateExportBody() filters = list_body.model_dump( by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True, ) rows = export_contract_templates(db, tenant_no, filters=filters) rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None) or getattr(body, "ids", None)) export_cols = filter_export_columns(CONTRACT_TEMPLATE_EXPORT_COLUMNS, getattr(list_body, "columns", None) or getattr(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 ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) # --- 审批记录独立页 --- @router.get("/contract/approval/list") async def contract_approval_list( pageNum: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=500), contractId: Optional[str] = None, contractNo: Optional[str] = None, contractName: Optional[str] = None, approverName: Optional[str] = None, approvalResult: 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) query = ContractApprovalListQuery( pageNum=pageNum, pageSize=pageSize, contractId=contractId, contractNo=contractNo, contractName=contractName, approverName=approverName, approvalResult=approvalResult, beginTime=beginTime, endTime=endTime, ) rows, total = list_contract_approvals_page( db, tenant_no, page=query.pageNum, page_size=query.pageSize, filters=query.model_dump(by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True), ) return success_response( { "list": rows, "rows": rows, "total": total, "pageNum": query.pageNum, "pageSize": query.pageSize, "totalPages": total_pages(total, query.pageSize), } ) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.get("/contract/approval/{approval_id}") async def contract_approval_detail( approval_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) detail = get_contract_approval_detail(db, approval_id, tenant_no=tenant_no) return success_response(detail) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/approval/pass") async def contract_approval_pass( body: ContractApprovalPassBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: user_id = _user.get("user_id", "") or _user.get("id", "") approve_pass(db, body.id, approval_opinion=body.approvalOpinion 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.post("/contract/approval/reject") async def contract_approval_reject( body: ContractApprovalRejectBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: user_id = _user.get("user_id", "") or _user.get("id", "") approve_reject(db, body.id, approval_opinion=body.approvalOpinion, 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.post("/contract/approval/transfer") async def contract_approval_transfer( body: ContractApprovalTransferBody, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: user_id = _user.get("user_id", "") or _user.get("id", "") approve_transfer( db, body.id, target_approver_id=body.targetApproverId, transfer_reason=body.transferReason, 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.post("/contract/approval/export") async def contract_approval_export( body: ContractApprovalExportBody = 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 ContractApprovalExportBody() filters = list_body.model_dump( by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True, ) rows = export_contract_approvals(db, tenant_no, filters=filters) rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None) or getattr(body, "ids", None)) export_cols = filter_export_columns(CONTRACT_APPROVAL_EXPORT_COLUMNS, getattr(list_body, "columns", None) or getattr(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 ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/contract/approval/refreshNames") async def contract_approval_refresh_names( body: ContractApprovalRefreshNamesBody = None, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: req = body or ContractApprovalRefreshNamesBody() count = refresh_approver_names(db, contract_id=req.contractId) db.commit() return success_response(count) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e))