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 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 제시가(원). 세션 목표가 산정 최우선값 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 Req_AwardQuotation(QuotationProtocol): winner_supplier_id: 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 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 anchoring_price: Optional[int] = None # 앵커링가(원). 목표가×(1000−anchoring_value)//1000 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 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 anchoring_value: float = 0.0 # 앵커링율(비율). 세션 앵커링값(‰)을 /1000 환산 — main 프론트 표시용 candidates: list[TargetCandidate] = [] chosen_basis: Optional[str] = None target_price: int = 0 anchoring_price: Optional[int] = None