- partner.supplier_items 매핑테이블 신설: supply_type(0없음/1유통/2제조/3총판), 부분 유니크 인덱스(soft-delete 인지). quotations.supplier_type와 의미단위 달라 컬럼명 supply_type. - 백엔드: supplier_item CRUD/service/router(/v1/supplier-item, by-supplier·by-item·create·bulk·update·delete). 소유권 company 스코프. ErrorType.SUPPLIER_ITEM_NOT_FOUND 추가. - DDL: 01-schema/04-alter(날짜접미 리네임) + dev DB 반영. - 프론트(feature): 협력사 엑셀 '취급상품' 컬럼(콤마 상품명→일괄매핑, 이름 미매칭 스킵+리포트, 유형 안받고 없음), 협력사 상세 취급상품 관리(추가/삭제/유형변경 즉시반영), 견적모달 협력사 리스트에 선택상품 공급유형 배지. - 재사용 서버검색 Combobox 신설 → 취급상품·견적상품·협력사초청·협상카드 픽리스트에 적용(size 100 캡 해소, 검색은 서버 ILIKE). 견적 선택상품 파생값은 useGetItem 단건조회로 안정화. - orval 재생성(supplier-item 클라이언트/모델). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
176 lines
7.2 KiB
Python
176 lines
7.2 KiB
Python
from abc import ABC, abstractmethod
|
|
from typing import Tuple
|
|
|
|
from sqlalchemy import select, func, update
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.database.model.models import supplier_items, items
|
|
from common.enums import ErrorType
|
|
from common.logger import LOG
|
|
from common.utils.gtime import GTime
|
|
|
|
|
|
# 협력사-상품 매핑 CRUD. 스코프는 상위(협력사/상품)가 company_id 로 이미 걸린다.
|
|
class ISupplierItemCRUD(ABC):
|
|
@abstractmethod
|
|
async def list_by_supplier(self, cdb: AsyncSession, supplier_id) -> Tuple[ErrorType, list]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def list_by_item(self, cdb: AsyncSession, item_id) -> Tuple[ErrorType, list]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def get_by_id(self, cdb: AsyncSession, supplier_item_id) -> Tuple[ErrorType, supplier_items]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def existing_item_ids(self, cdb: AsyncSession, supplier_id, item_ids: list) -> Tuple[ErrorType, list]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def find_items_by_names(self, cdb: AsyncSession, company_id, names: list) -> Tuple[ErrorType, list]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def add_many(self, cdb: AsyncSession, mappings: list) -> ErrorType:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def update_type(self, cdb: AsyncSession, supplier_item_id, supply_type: int) -> ErrorType:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def soft_delete(self, cdb: AsyncSession, supplier_item_id) -> ErrorType:
|
|
pass
|
|
|
|
|
|
class SupplierItemCRUD(ISupplierItemCRUD):
|
|
async def list_by_supplier(self, cdb: AsyncSession, supplier_id) -> Tuple[ErrorType, list]:
|
|
# 협력사 상세용: 매핑 + 상품명/코드 조인. Row(supplier_item_id, item_id, name, code, supply_type)
|
|
try:
|
|
query = (
|
|
select(
|
|
supplier_items.supplier_item_id,
|
|
supplier_items.item_id,
|
|
items.name,
|
|
items.code,
|
|
supplier_items.supply_type,
|
|
)
|
|
.join(items, items.item_id == supplier_items.item_id)
|
|
.where(
|
|
supplier_items.supplier_id == supplier_id,
|
|
supplier_items.deleted == False, # noqa: E712
|
|
items.deleted == False, # noqa: E712
|
|
)
|
|
.order_by(supplier_items.created_at.desc())
|
|
)
|
|
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
|
|
if err_type != ErrorType.SUCCESS:
|
|
return err_type, []
|
|
return ErrorType.SUCCESS, list(rows)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, []
|
|
|
|
async def list_by_item(self, cdb: AsyncSession, item_id) -> Tuple[ErrorType, list]:
|
|
# 견적생성 모달용: 이 상품을 취급하는 협력사별 공급유형. Row(supplier_id, supply_type)
|
|
try:
|
|
query = select(supplier_items.supplier_id, supplier_items.supply_type).where(
|
|
supplier_items.item_id == item_id,
|
|
supplier_items.deleted == False, # noqa: E712
|
|
)
|
|
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
|
|
if err_type != ErrorType.SUCCESS:
|
|
return err_type, []
|
|
return ErrorType.SUCCESS, list(rows)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, []
|
|
|
|
async def get_by_id(self, cdb: AsyncSession, supplier_item_id) -> Tuple[ErrorType, supplier_items]:
|
|
try:
|
|
query = select(supplier_items).where(
|
|
supplier_items.supplier_item_id == supplier_item_id,
|
|
supplier_items.deleted == False, # noqa: E712
|
|
).limit(1)
|
|
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query)
|
|
if err_type != ErrorType.SUCCESS:
|
|
return err_type, None
|
|
if len(row_list) != 1:
|
|
return ErrorType.DB_INVALID_KEY, None
|
|
return ErrorType.SUCCESS, row_list[0]
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, None
|
|
|
|
async def existing_item_ids(self, cdb: AsyncSession, supplier_id, item_ids: list) -> Tuple[ErrorType, list]:
|
|
# 이 협력사에 이미 매핑된 item_id 들(중복 등록 스킵용).
|
|
try:
|
|
if not item_ids:
|
|
return ErrorType.SUCCESS, []
|
|
query = select(supplier_items.item_id).where(
|
|
supplier_items.supplier_id == supplier_id,
|
|
supplier_items.item_id.in_(item_ids),
|
|
supplier_items.deleted == False, # noqa: E712
|
|
)
|
|
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
|
|
if err_type != ErrorType.SUCCESS:
|
|
return err_type, []
|
|
return ErrorType.SUCCESS, [r for r in rows if r is not None]
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, []
|
|
|
|
async def find_items_by_names(self, cdb: AsyncSession, company_id, names: list) -> Tuple[ErrorType, list]:
|
|
# 상품명(정확 일치) → 상품. Row(item_id, name). 이름 중복 상품이 있으면 여럿 반환될 수 있다(서비스에서 첫 매칭 사용).
|
|
try:
|
|
if not names:
|
|
return ErrorType.SUCCESS, []
|
|
query = select(items.item_id, items.name).where(
|
|
items.company_id == company_id,
|
|
items.name.in_(names),
|
|
items.deleted == False, # noqa: E712
|
|
)
|
|
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
|
|
if err_type != ErrorType.SUCCESS:
|
|
return err_type, []
|
|
return ErrorType.SUCCESS, list(rows)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, []
|
|
|
|
async def add_many(self, cdb: AsyncSession, mappings: list) -> ErrorType:
|
|
try:
|
|
if not mappings:
|
|
return ErrorType.SUCCESS
|
|
return await DB_SESSION_MNG.insert(cdb, mappings)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED
|
|
|
|
async def update_type(self, cdb: AsyncSession, supplier_item_id, supply_type: int) -> ErrorType:
|
|
try:
|
|
query = (
|
|
update(supplier_items)
|
|
.where(supplier_items.supplier_item_id == supplier_item_id)
|
|
.values(supply_type=supply_type, updated_at=GTime.UTC())
|
|
)
|
|
return await DB_SESSION_MNG.add(cdb, query)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED
|
|
|
|
async def soft_delete(self, cdb: AsyncSession, supplier_item_id) -> ErrorType:
|
|
try:
|
|
query = (
|
|
update(supplier_items)
|
|
.where(supplier_items.supplier_item_id == supplier_item_id)
|
|
.values(deleted=True, updated_at=GTime.UTC())
|
|
)
|
|
return await DB_SESSION_MNG.add(cdb, query)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED
|