- 견적유형 4종: QuotationType 에 신규협상(3)·신규견적(4) 추가(1·2 재 고정)
- 목표가 산정: items(매입가·판매가·인터넷최저가) + quotations.md_price +
quotation_settings.internet_average_fee 로 _calc_target_price(KTC 이식, 신규/재 분리),
세션 target_price·target_anchoring_price 저장
- 협력사유형: quotations.supplier_type(SupplierType) + 직전유형 조회 API(/supplier/{id}/last-type)
- 협상카드 용도: nego_cards/wild_cards.usage_type(CardUsageType: 공통/신규견적전용/재견적전용)
- 스키마(01-schema.sql)·ORM·protocol·service·crud·프론트(생성타입/폼) 일괄, /backend enums·models 미러
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
256 lines
13 KiB
Python
256 lines
13 KiB
Python
import uuid
|
||
|
||
from sqlalchemy.orm import declarative_base
|
||
from sqlalchemy import Column, Integer, SmallInteger, BigInteger, Numeric, Float, String, Boolean, DateTime
|
||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||
from sqlalchemy.sql import text
|
||
|
||
from common.enums import DBType, UserStatus, UserRole, CompanyStatus
|
||
|
||
# 모든 ORM 모델의 베이스. insert 시 isinstance 체크에도 사용된다.
|
||
MAIN_BASE = declarative_base()
|
||
|
||
|
||
# 공통 mixin
|
||
# DB 계약(_DBTypeMixin)과 ERD 공통 컬럼(MainTableMixin)을 분리해 둔다.
|
||
def _utc_now_sql():
|
||
return text("(now() AT TIME ZONE 'utc')")
|
||
|
||
|
||
class _DBTypeMixin:
|
||
"""모델이 자신이 속한 논리 DB 를 알려준다 (람다 실행 시 DBType 으로 세션 선택)."""
|
||
|
||
@staticmethod
|
||
def DBType():
|
||
return DBType.MAIN.value
|
||
|
||
|
||
# ERD 공통 컬럼
|
||
class MainTableMixin(_DBTypeMixin):
|
||
created_at = Column(DateTime(timezone=True), nullable=False, server_default=_utc_now_sql())
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=_utc_now_sql(), onupdate=_utc_now_sql())
|
||
deleted = Column(Boolean, nullable=False, server_default=text("false"), default=False)
|
||
|
||
|
||
# ERD 도메인 모델
|
||
class companies(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "companies"
|
||
__table_args__ = {"schema": "company"}
|
||
|
||
company_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
name = Column(String(100), nullable=False)
|
||
business_number = Column(String(30), nullable=True)
|
||
code = Column(Integer, nullable=True) # 내부 인덱스용
|
||
representative_name = Column(String(50), nullable=True)
|
||
email = Column(String(255), nullable=True)
|
||
contact_number = Column(String(20), nullable=True)
|
||
website_url = Column(String(255), nullable=True)
|
||
industry = Column(SmallInteger, nullable=True) # 업종 코드 (스키마 SMALLINT)
|
||
status = Column(SmallInteger, nullable=False, default=CompanyStatus.ACTIVE.value) # CompanyStatus
|
||
|
||
|
||
class users(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "users"
|
||
__table_args__ = {"schema": "company"}
|
||
|
||
user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
company_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||
id = Column(String(20), nullable=False, unique=True, index=True) # 로그인 아이디
|
||
password = Column(String(255), nullable=False) # bcrypt 해시 (ERD VARCHAR(30)→255 확장)
|
||
name = Column(String(50), nullable=True)
|
||
email = Column(String(255), nullable=True)
|
||
contact_number = Column(String(20), nullable=True)
|
||
last_accessed_at = Column(DateTime(timezone=True), nullable=False, server_default=_utc_now_sql())
|
||
status = Column(SmallInteger, nullable=False, default=UserStatus.ACTIVE.value)
|
||
role = Column(SmallInteger, nullable=False, default=UserRole.USER.value)
|
||
|
||
|
||
class items(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "items"
|
||
__table_args__ = {"schema": "partner"}
|
||
|
||
item_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
company_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||
user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 등록한 유저
|
||
|
||
name = Column(String(100), nullable=False)
|
||
code = Column(String(30), nullable=True)
|
||
category = Column(String(255), nullable=True)
|
||
category_type = Column(Integer, nullable=False, default=1) # 카테고리 탐색용 자동 증가 숫자
|
||
|
||
image_url = Column(String(255), nullable=True)
|
||
model_name = Column(String(100), nullable=True)
|
||
spec = Column(String(255), nullable=True)
|
||
manufacturer = Column(String(50), nullable=True)
|
||
made_in = Column(String(100), nullable=True)
|
||
|
||
price = Column(BigInteger, nullable=True) # 금액(원), 스키마 BIGINT
|
||
internet_lowest_price_yn = Column(Boolean, nullable=False, default=False) # 최저가 솔루션 원자성 보존용
|
||
internet_lowest_price = Column(BigInteger, nullable=True) # 인터넷 최저가 실값(원). 목표가 산정용(KTC: ×(1−수수료))
|
||
purchase_price = Column(BigInteger, nullable=True) # 매입가(원). 목표가 후보(그대로). KTC items.purchase_price(Integer)
|
||
selling_price = Column(BigInteger, nullable=True) # 판매가(원). 목표가 후보(×(1−마진율)), 유통형만. KTC items.selling_price(Integer)
|
||
|
||
moq = Column(String(50), nullable=True) # 최소 주문 수량
|
||
lead_time = Column(SmallInteger, nullable=True) # 주문 후 배송 도착까지 시간
|
||
quantity_unit = Column(String(50), nullable=True) # 단위 라벨(자유입력): EA/BOX/SET/ROLL ...
|
||
delivery_type = Column(SmallInteger, nullable=True) # 배송 유형 코드
|
||
vat_yn = Column(Boolean, nullable=True)
|
||
delivery_fee_yn = Column(Boolean, nullable=True)
|
||
|
||
|
||
class suppliers(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "suppliers"
|
||
__table_args__ = {"schema": "partner"}
|
||
|
||
supplier_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
company_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||
user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 등록한 유저
|
||
|
||
name = Column(String(100), nullable=False)
|
||
code = Column(String(20), nullable=True)
|
||
manager_name = Column(String(50), nullable=True)
|
||
manager_email = Column(String(255), nullable=True)
|
||
manager_contact_number = Column(String(20), nullable=True) # ERD 오타(manger) 교정
|
||
priority = Column(String(10), nullable=True) # True/False 가 아닌 string value 가능
|
||
|
||
|
||
class nego_cards(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "nego_cards"
|
||
__table_args__ = {"schema": "card"}
|
||
|
||
nego_card_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
user_id = Column(UUID(as_uuid=True), nullable=True, index=True) # 등록 유저(o2o 기본 카드는 NULL)
|
||
name = Column(String(20), nullable=True) # 카드명
|
||
number = Column(String(10), nullable=True) # 식별번호(카드코드)
|
||
script = Column(String(255), nullable=True) # 협상 스크립트(평문 미리보기)
|
||
edit_script = Column(JSONB, nullable=True) # 편집된 스크립트(Slate JSON)
|
||
usage_type = Column(SmallInteger, nullable=False, default=1) # 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용
|
||
|
||
|
||
class wild_cards(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "wild_cards"
|
||
__table_args__ = {"schema": "card"}
|
||
|
||
wild_card_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
user_id = Column(UUID(as_uuid=True), nullable=True, index=True) # 등록 유저(o2o 기본 카드는 NULL)
|
||
name = Column(String(20), nullable=True) # 카드명
|
||
number = Column(String(10), nullable=True) # 식별번호(카드코드)
|
||
script = Column(String(255), nullable=True) # 협상 스크립트(평문 미리보기)
|
||
edit_script = Column(JSONB, nullable=True) # 편집된 스크립트(Slate JSON)
|
||
usage_type = Column(SmallInteger, nullable=False, default=1) # 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용
|
||
condition = Column(String(255), nullable=True) # 사용 조건(트리거)
|
||
available = Column(Boolean, nullable=False, default=False) # 수동 협상 적용 여부(ACTIVE/INACTIVE 매핑)
|
||
memo = Column(String(255), nullable=True) # 자유 메모
|
||
|
||
|
||
class versions(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "versions"
|
||
__table_args__ = {"schema": "card"}
|
||
|
||
version_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 버전 생성 유저
|
||
code = Column(Integer, nullable=False, default=0) # 빠른 조회용
|
||
name = Column(String(10), nullable=False) # 버전명
|
||
|
||
|
||
class version_nego_cards(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "version_nego_cards"
|
||
__table_args__ = {"schema": "card"}
|
||
|
||
vnc_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
version_id = Column(UUID(as_uuid=True), nullable=False, index=True) # versions.version_id
|
||
nego_card_id = Column(UUID(as_uuid=True), nullable=False, index=True) # nego_cards.nego_card_id
|
||
|
||
|
||
class version_wild_cards(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "version_wild_cards"
|
||
__table_args__ = {"schema": "card"}
|
||
|
||
vwc_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
version_id = Column(UUID(as_uuid=True), nullable=False, index=True) # versions.version_id
|
||
wild_card_id = Column(UUID(as_uuid=True), nullable=False, index=True) # wild_cards.wild_card_id
|
||
|
||
|
||
class quotation_settings(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "quotation_settings"
|
||
__table_args__ = {"schema": "quotation"}
|
||
|
||
qt_setting_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
user_id = Column(UUID(as_uuid=True), nullable=True, index=True) # 설정 소유 유저
|
||
target_margin_rate = Column(Numeric(8, 6), nullable=False)
|
||
anchoring_value = Column(Numeric(8, 6), nullable=False, default=0.01)
|
||
internet_average_fee = Column(Numeric(8, 6), nullable=False, default=0.078) # 인터넷 평균 수수료율. 목표가=인터넷최저가×(1−값). KTC QuotationPrice.internet_average_fee(Float)
|
||
card_count = Column(Integer, nullable=False, default=3) # 한 협상 내 협상카드 사용 횟수
|
||
|
||
|
||
class quotations(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "quotations"
|
||
__table_args__ = {"schema": "quotation"}
|
||
|
||
qt_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||
qt_setting_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||
version_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||
|
||
name = Column(String(50), nullable=False)
|
||
number = Column(String(30), nullable=False)
|
||
type = Column(SmallInteger, nullable=False) # QuotationType: 1=renego(1:1) / 2=requote(1:N)
|
||
round = Column(Integer, nullable=False, default=1) # 재견적 진행 시 증가
|
||
status = Column(SmallInteger, nullable=False) # 진행 상태 코드
|
||
start_time = Column(DateTime(timezone=True), nullable=False)
|
||
end_time = Column(DateTime(timezone=True), nullable=False)
|
||
|
||
manager_name = Column(String(50), nullable=True)
|
||
manager_email = Column(String(255), nullable=True)
|
||
manager_contact_number = Column(String(20), nullable=True)
|
||
memo = Column(String(100), nullable=True)
|
||
md_price = Column(BigInteger, nullable=True) # MD 제시가(원). 목표가 산정 최우선값 — 입력은 견적생성 모달
|
||
supplier_type = Column(SmallInteger, nullable=True) # 협력사 유형(SupplierType). 재견적은 1:1이라 견적에 박는다. 입력은 견적생성 모달
|
||
|
||
iteration = Column(Integer, nullable=False, default=0)
|
||
preferred_sp_yn = Column(Boolean, nullable=True)
|
||
preferred_sp_id = Column(UUID(as_uuid=True), nullable=True)
|
||
preferred_sp_name = Column(String(20), nullable=True)
|
||
equal_bid_yn = Column(Boolean, nullable=True)
|
||
equal_bid_data = Column(JSONB, nullable=True)
|
||
|
||
|
||
class sessions(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "sessions"
|
||
__table_args__ = {"schema": "negotiation"}
|
||
|
||
session_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
quotation_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 소속 견적(quotations.qt_id)
|
||
item_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 대상 상품(items.item_id)
|
||
supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 대상 공급사(suppliers.supplier_id)
|
||
|
||
qt_number = Column(String(30), nullable=False) # 견적번호 스냅샷
|
||
qt_round = Column(Integer, nullable=False) # 견적 라운드 스냅샷
|
||
qt_type = Column(SmallInteger, nullable=False) # QuotationType 스냅샷
|
||
target_price = Column(BigInteger, nullable=False) # 목표가(원)
|
||
target_anchoring_price = Column(BigInteger, nullable=True) # 앵커링가(원)=floor(목표가×(1−anchoring_value)). KTC sessions.target_anchoring_price(BigInteger)
|
||
status = Column(SmallInteger, nullable=False) # SessionStatus 코드
|
||
bid_price = Column(BigInteger, nullable=True) # 입찰가(원)
|
||
bid_at = Column(DateTime(timezone=True), nullable=True) # 입찰 시각
|
||
end_time = Column(DateTime(timezone=True), nullable=False) # 세션 종료 시각
|
||
reject_reason = Column(String(255), nullable=True)
|
||
reject_price = Column(BigInteger, nullable=True)
|
||
reject_delivery_type = Column(SmallInteger, nullable=True) # DeliveryType 코드
|
||
|
||
|
||
class chats(MainTableMixin, MAIN_BASE):
|
||
__tablename__ = "chats"
|
||
__table_args__ = {"schema": "negotiation"}
|
||
|
||
chat_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
session_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 소속 세션(sessions.session_id)
|
||
card_id = Column(UUID(as_uuid=True), nullable=True) # 사용된 카드(nego_cards.nego_card_id)
|
||
|
||
seq = Column(Integer, nullable=False, default=1) # 세션 내 메시지 순번(프로토콜 index)
|
||
sender = Column(SmallInteger, nullable=False) # ChatSender 코드(1=bot, 2=partner)
|
||
target_price = Column(BigInteger, nullable=False) # 제시 목표가(원)
|
||
card_used_yn = Column(Boolean, nullable=True) # 카드 사용 여부
|
||
indicator_value = Column(Numeric(8, 6), nullable=True)
|
||
card_type = Column(SmallInteger, nullable=True) # 1=nego_card, 2=wild_card
|
||
meta = Column(JSONB, nullable=True) # 말풍선 표현 데이터(script/step/client_step/input_mode/input_options/chat_end)
|