import uuid from datetime import datetime from typing import Any, Optional from pydantic import ConfigDict from common.enums import CardType, ChatSender, CloseReason, DeliveryType, PriceGateAction, QuotationStatus, QuotationType, SessionStatus, SupplierType from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol class QuotationProtocol(WebPacketProtocol): pass class Req_CreateQuotation(QuotationProtocol): qt_setting_id: uuid.UUID version_id: Optional[uuid.UUID] = None # 미지정 시 기본 전략 버전(card.versions 시드)으로 채움 name: str = "" type: int = 0 status: int = 0 start_time: Optional[datetime] = None # 미지정 시 생성 시각(UTC) end_time: datetime round: int = 1 manager_name: Optional[str] = None manager_email: Optional[str] = None manager_contact_number: Optional[str] = None memo: Optional[str] = None md_price: Optional[int] = None # MD 제시가(원). 세션 목표가 산정 최우선값 supplier_type: Optional[int] = None # 협력사 유형(SupplierType). 재견적 1:1 → 견적에 기록 item_ids: list[uuid.UUID] = [] # 협상 대상 상품. item×supplier 조합마다 세션 1개 생성 supplier_ids: list[uuid.UUID] = [] # 협상 초청 공급사 card_ids: list[uuid.UUID] = [] # 선택 협상카드. 버전을 만들어 묶고 quotation.version_id 로 연결 # 낙찰 기준 — 1:1 협상만 프론트가 2전략(앵커까지/목표까지)을 mid/over 로 전개해 전송(over 는 항상 OPEN). # 1:N 경매는 미전송 → 서버가 mid=over=AWARD 강제('무조건 최저가 낙찰'). mid_action: Optional[int] = None # PriceGateAction: 앵커링가<투찰가≤목표가 처리 over_action: Optional[int] = None # PriceGateAction: 목표가<투찰가 처리 class Req_RegenerateQuotation(QuotationProtocol): supplier_ids: list[uuid.UUID] = [] # 다음 라운드에 부를 공급사(프론트 선택). 상품·기간·번호는 원 견적에서 이어받음 class QuotationData(WebPacketProtocol): model_config = ConfigDict(from_attributes=True) qt_id: uuid.UUID user_id: uuid.UUID qt_setting_id: uuid.UUID version_id: uuid.UUID name: str number: str type: QuotationType round: int = 1 status: QuotationStatus start_time: datetime end_time: datetime manager_name: Optional[str] = None manager_email: Optional[str] = None manager_contact_number: Optional[str] = None memo: Optional[str] = None md_price: Optional[int] = None supplier_type: Optional[SupplierType] = None iteration: int = 0 preferred_sp_yn: Optional[bool] = None preferred_sp_id: Optional[uuid.UUID] = None preferred_sp_name: Optional[str] = None equal_bid_yn: Optional[bool] = None equal_bid_data: Optional[Any] = None close_reason: Optional[CloseReason] = None # 마감 사유(CloseReason). 미마감이면 None mid_action: Optional[int] = None # 낙찰 기준(견적 단위). 상세 드로어 낙찰기준 표시용 over_action: Optional[int] = None participation_count: int = 0 # 견적별 참여 협력사 수(세션 distinct supplier). 목록 집계로 채움. item_id: Optional[uuid.UUID] = None # 대표 상품 id(세션의 첫 item). 목록 조인으로 채움. item_name: Optional[str] = None # 대표 상품명. 목록 조인으로 채움. creator_name: Optional[str] = None # 등록자(작성자) 이름. user_id→company.users.name 조인으로 채움. created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class Res_Quotation(Res_WebPacketProtocol): quotation: Optional[QuotationData] = None class Res_QuotationList(Res_PageProtocol): quotations: list[QuotationData] = [] class Res_DeleteQuotation(Res_WebPacketProtocol): pass class SessionData(WebPacketProtocol): model_config = ConfigDict(from_attributes=True) session_id: uuid.UUID qt_id: uuid.UUID supplier_id: uuid.UUID item_id: uuid.UUID qt_number: str qt_round: int qt_type: QuotationType target_price: int target_anchoring_price: Optional[int] = None # 앵커링가(원). 목표가×(1−앵커링율) status: SessionStatus bid_price: Optional[int] = None bid_at: Optional[datetime] = None end_time: datetime reject_reason: Optional[str] = None reject_price: Optional[int] = None reject_delivery_type: Optional[DeliveryType] = None email_sent_at: Optional[datetime] = None # 협상 초청 메일 발송 시각(None=미발송). 프론트 발송배지/재발송 판단 url: str = "" # 세션 chat 실행 URL(공급사 협상 프론트). DB 미저장 — session_id 로 구성 class Res_CreateQuotation(Res_WebPacketProtocol): qt_id: Optional[uuid.UUID] = None # 생성된 견적 id(프론트가 상세 오버레이 오픈에 사용) session_count: int = 0 # 함께 생성된 협상 세션 수(토스트 표시용). 세션 풀바디는 별도 GET 으로 조회 class Res_NotifySessions(Res_WebPacketProtocol): sent: int = 0 # 발송 성공 세션 수 failed: int = 0 # 발송 시도했으나 실패한 세션 수 skipped: int = 0 # 담당자 이메일이 없어 건너뛴 세션 수 total: int = 0 # 대상 세션 총수 class Res_QuotationStatus(Res_WebPacketProtocol): qt_id: Optional[uuid.UUID] = None job_status: int = 0 message: str = "" class Res_QuotationSessions(Res_WebPacketProtocol): qt_id: Optional[uuid.UUID] = None sessions: list[SessionData] = [] total: int = 0 class ChatMessageData(WebPacketProtocol): model_config = ConfigDict(from_attributes=True) chat_id: uuid.UUID session_id: uuid.UUID card_id: Optional[uuid.UUID] = None index: int sender: ChatSender target_price: int card_used_yn: Optional[bool] = None indicator_value: Optional[float] = None card_type: Optional[CardType] = None script: Optional[str] = None step: Optional[str] = None class Res_SessionChat(Res_WebPacketProtocol): session_id: Optional[uuid.UUID] = None messages: list[ChatMessageData] = [] class Res_QuotationResult(Res_WebPacketProtocol): qt_id: Optional[uuid.UUID] = None winner_supplier_id: Optional[uuid.UUID] = None winner_supplier_name: Optional[str] = None is_equal_bid: Optional[bool] = None equal_bid_data: Optional[Any] = None result_count: int = 0 class QuotationCardData(WebPacketProtocol): session_card_id: uuid.UUID qt_id: Optional[uuid.UUID] = None nego_card_id: Optional[uuid.UUID] = None wild_card_id: Optional[uuid.UUID] = None type: Optional[CardType] = None number: Optional[str] = None name: Optional[str] = None script: Optional[str] = None # 협상 멘트(평문) edit_script: Optional[Any] = None # 협상 멘트(Slate 서식본) condition: Optional[str] = None # 와일드카드 전용: 사용 조건(트리거) memo: Optional[str] = None # 와일드카드 전용: 메모 class Res_QuotationCards(Res_WebPacketProtocol): qt_id: Optional[uuid.UUID] = None cards: list[QuotationCardData] = [] class Res_LastSupplierType(Res_WebPacketProtocol): supplier_type: Optional[SupplierType] = None # 협력사 직전 견적의 유형(없으면 None) qt_number: Optional[str] = None # 그 견적의 번호(이전 견적 값임을 표시용) class TargetCandidate(WebPacketProtocol): basis: str label: str value: int class Res_TargetBreakdown(Res_WebPacketProtocol): is_new: bool = False is_inherited: bool = False md_price: Optional[int] = None internet_lowest: Optional[int] = None purchase: Optional[int] = None selling: Optional[int] = None fee: float = 0.0 margin: float = 0.0 candidates: list[TargetCandidate] = [] chosen_basis: Optional[str] = None target_price: int = 0 target_anchoring_price: Optional[int] = None