import uuid from datetime import datetime from typing import Any, Optional from pydantic import ConfigDict from common.enums import AwardType, 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: 목표가<투찰가 처리 done_ceiling_rate: Optional[int] = None # 협상 완료 상한율(‰) 견적 override. None 이면 회사 세팅값 class Req_RegenerateQuotation(QuotationProtocol): supplier_ids: list[uuid.UUID] = [] # 다음 라운드에 부를 공급사(프론트 선택). 상품·번호는 원 견적에서 이어받음 # 아래 재지정값은 전부 미전송(None)이면 원 견적/직전 라운드 값을 그대로 승계한다. card_ids: Optional[list[uuid.UUID]] = None # 다음 라운드 협상카드. 빈 리스트면 카드 없는 새 버전 target_price: Optional[int] = None # 목표가(원). 이 라운드의 모든 상품에 적용 end_time: Optional[datetime] = None # 마감기한. 미전송이면 원 견적과 같은 기간 길이로 생성 시각부터 done_ceiling_rate: Optional[int] = None # 타결 상한율(‰). 완료 상한=목표가×(1+값/1000) class Req_AwardQuotation(QuotationProtocol): winner_supplier_id: uuid.UUID # 담당자가 직접 낙찰시킬 협력사(견적에 초청된 협력사 중 선택) contract_price: int # 최종 계약가(원). 오프라인 협상 결과를 담당자가 확정해 넣는다 contract_note: Optional[str] = None # 계약가 근거 메모(오프라인 협상 요약) 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 award_type: Optional[AwardType] = None # 낙찰 방식(AwardType 1=자동/2=직접). 미낙찰이면 None custom: Optional[dict] = None # 견적 부가정보. 직접 낙찰 시 award={reason,by,at}(사유·처리자·시각) mid_action: Optional[int] = None # 낙찰 기준(견적 단위). 상세 드로어 낙찰기준 표시용 over_action: Optional[int] = None done_ceiling_rate: Optional[int] = None # 타결 상한율(‰) 견적 override. 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 done_ceiling_price: Optional[int] = None # 완료 상한가(원). 생성 시 박제 = 목표가×(1+완료상한율/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 contract_price: Optional[int] = None # 직접 낙찰 계약가(원). 자동낙찰은 None(bid_price 가 계약가) email_sent_at: Optional[datetime] = None # 협상 초청 메일 발송 시각(None=미발송). 프론트 발송배지/재발송 판단 custom: Optional[dict] = None # 협상완료 부가정보 값 {key: value} (공급사가 타결 후 입력, 정의는 companies.settings.session_fields) 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] = [] hidden_price_fields: list[str] = [] # 회사 설정으로 감춰 후보에서 뺀 가격 필드 chosen_basis: Optional[str] = None target_price: int = 0 anchoring_price: Optional[int] = None