"""CRM 报价单 API 路由 — 挂接到 crm.router""" from __future__ import annotations from datetime import datetime from fastapi import Depends 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.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.quote import ( accept_quote, create_new_version, create_quote, generate_pdf, generate_share, get_quote_detail, get_template, invalid_quote, list_quotes, query_versions, reject_quote, save_template, soft_delete_quotes, statistics_overview, statistics_status, statistics_trend, submit_quote, to_contract, total_pages as quote_total_pages, update_quote, ) from app.crud.crm.quote_approver_setting import get_setting, save_setting from app.crud.crm.quote_approval import ( approve_quote, list_quote_approvals, refresh_approver_names, withdraw_approval, ) from app.crud.crm.quote_product import ( insert_quote_product, list_quote_products, soft_delete_quote_products, update_quote_product, ) from app.crud.crm.quote_share_log import list_share_logs, share_log_statistics from app.schemas.crm.quote_approver_setting import QuoteApproverSettingSaveBody from app.schemas.crm.quote import ( QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody, QuoteCreateBody, QuoteDeleteBody, QuoteDetailBody, QuoteInvalidBody, QuoteListBody, QuoteNewVersionBody, QuotePdfBody, QuoteProductCreateBody, QuoteProductDeleteBody, QuoteProductListBody, QuoteProductUpdateBody, QuoteRejectBody, QuoteShareGenerateBody, QuoteShareLogListBody, QuoteShareLogStatisticsBody, QuoteStatisticsOverviewBody, QuoteStatisticsTrendBody, QuoteSubmitBody, QuoteTemplateGetBody, QuoteTemplateSaveBody, QuoteToContractBody, QuoteUpdateBody, QuoteVersionsBody, ) from app.utils.crm_export_columns import QUOTE_EXPORT_COLUMNS from app.utils.excel_export import ( EXPORT_MAX_ROWS, ExportValidationError, export_rows_to_xlsx_response, filter_export_columns, ) from app.api.crm import _filter_export_rows_by_ids def register_quote_routes(router): @router.post("/quote/list") async def quote_list( body: QuoteListBody = 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 QuoteListBody() user_id = _user.get("user_id", "") or _user.get("id", "") rows, total = list_quotes( db, tenant_no, user_info=user_info, page=list_body.page, page_size=list_body.pageSize, filters=list_body.model_dump(by_alias=True, exclude={"page", "pageSize"}), current_user_id=user_id, ) tp = quote_total_pages(total, list_body.pageSize) return success_response( { "list": rows, "rows": rows, "total": total, "page": list_body.page, "pageSize": list_body.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("/quote/detail") async def quote_detail( body: QuoteDetailBody, _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_quote_detail(db, body.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("/quote/add") async def quote_add( body: QuoteCreateBody, _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_quote( db, tenant_no=tenant_no, data=body.model_dump(by_alias=True), user_id=user_id, dept_id=dept_id, ) 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.post("/quote/update") async def quote_update( body: QuoteUpdateBody, _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) quote_id = data.pop("id") detail = update_quote( db, quote_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.post("/quote/delete") async def quote_delete( body: QuoteDeleteBody, _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) soft_delete_quotes(db, body.ids, 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("/quote/submit") async def quote_submit( body: QuoteSubmitBody, _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) submit_quote( db, body.id, tenant_no=tenant_no, approver_ids=body.approverIds, remark=body.remark, 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("/quote/saveTemplate") async def quote_save_template( body: QuoteTemplateSaveBody, _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) 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("/quote/getTemplate") async def quote_get_template( body: QuoteTemplateGetBody, _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, body.id, tenant_no=tenant_no, user_info=user_info) return success_response(content or "") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/quote/generatePdf") async def quote_generate_pdf( body: QuotePdfBody, _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) pdf_url = generate_pdf(db, body.id, tenant_no=tenant_no) return success_response(pdf_url) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/quote/generateShare") async def quote_generate_share( body: QuoteShareGenerateBody, _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) link = generate_share( db, body.id, tenant_no=tenant_no, validity_days=body.validityDays, password=body.password or "", user_info=user_info, ) return success_response(link) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/quote/accept") async def quote_accept( body: QuoteAcceptBody, _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) accept_quote(db, body.id, tenant_no=tenant_no, remark=body.remark) 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("/quote/reject") async def quote_reject( body: QuoteRejectBody, _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) reject_quote( db, body.id, tenant_no=tenant_no, reject_reason=body.rejectReason, remark=body.remark, 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("/quote/toContract") async def quote_to_contract( body: QuoteToContractBody, _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 = to_contract( db, body.id, tenant_no=tenant_no, contract_no=body.contractNo, contract_title=body.contractTitle, sign_date=body.signDate, start_date=body.startDate, end_date=body.endDate, user_id=user_id, dept_id=dept_id, user_info=user_info, ) return success_response(contract_id) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/quote/newVersion") async def quote_new_version( body: QuoteNewVersionBody, _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 = create_new_version( db, body.id, tenant_no=tenant_no, version=body.version, remark=body.remark, user_id=user_id, dept_id=dept_id, user_info=user_info, ) return success_response(new_id) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/quote/versions") async def quote_versions( body: QuoteVersionsBody, _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_versions(db, body.quoteNo, 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("/quote/invalid") async def quote_invalid( body: QuoteInvalidBody, _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) invalid_quote(db, body.id, tenant_no=tenant_no, remark=body.remark) 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("/quote/export") async def quote_export( body: QuoteListBody = 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 QuoteListBody() rows, _ = list_quotes( db, tenant_no, user_info=user_info, page=1, page_size=EXPORT_MAX_ROWS, filters=list_body.model_dump(by_alias=True, exclude={"page", "pageSize"}), ) rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None)) export_cols = filter_export_columns(QUOTE_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.post("/quote/statistics/overview") async def quote_statistics_overview( body: QuoteStatisticsOverviewBody = 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 = body or QuoteStatisticsOverviewBody() 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.post("/quote/statistics/status") async def quote_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_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.post("/quote/statistics/trend") async def quote_statistics_trend( body: QuoteStatisticsTrendBody = 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 = body or QuoteStatisticsTrendBody() 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.post("/quote/product/list") async def quote_product_list( body: QuoteProductListBody, _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_quote_products(db, body.quoteId, 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("/quote/product/add") async def quote_product_add( body: QuoteProductCreateBody, _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 "" obj = insert_quote_product( db, tenant_no=tenant_no, data=body.model_dump(by_alias=True), user_id=user_id, dept_id=dept_id, ) from app.crud.crm.quote_product import product_row_to_dict return success_response(product_row_to_dict(obj), message="新增成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/quote/product/update") async def quote_product_update( body: QuoteProductUpdateBody, _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) line_id = data.pop("id") obj = update_quote_product( db, line_id, tenant_no=tenant_no, data=data, user_id=user_id, user_info=user_info, ) from app.crud.crm.quote_product import product_row_to_dict return success_response(product_row_to_dict(obj), message="修改成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/quote/product/delete") async def quote_product_delete( body: QuoteProductDeleteBody, _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) soft_delete_quote_products(db, body.ids, 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("/quote/approval/list") async def quote_approval_list( body: QuoteApprovalListBody, _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_quote_approvals(db, body.quoteId, 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("/quote/approval/approve") async def quote_approval_approve( body: QuoteApprovalApproveBody, _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_quote( db, body.id, tenant_no=tenant_no, approval_result=body.approvalResult, approval_opinion=body.approvalOpinion or "", 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.post("/quote/approval/withdraw") async def quote_approval_withdraw( body: QuoteApprovalWithdrawBody, _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", "") withdraw_approval( db, body.quoteId, tenant_no=tenant_no, 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.post("/quote/approval/refreshNames") async def quote_approval_refresh_names( body: QuoteApprovalRefreshNamesBody = None, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: req = body or QuoteApprovalRefreshNamesBody() count = refresh_approver_names(db, quote_id=req.quoteId) return success_response(count) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/quote/shareLog/list") async def quote_share_log_list( body: QuoteShareLogListBody, _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_share_logs(db, body.quoteId, 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("/quote/shareLog/statistics") async def quote_share_log_statistics( body: QuoteShareLogStatisticsBody, _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) stats = share_log_statistics(db, body.quoteId, tenant_no=tenant_no) return success_response(stats) except ValueError as e: return error_response(400, str(e)) except Exception as e: return error_response(500, str(e)) @router.post("/quote-approver-setting/get") async def quote_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_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("/quote-approver-setting/save") async def quote_approver_setting_save( body: QuoteApproverSettingSaveBody, _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_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))