133 lines
5.8 KiB
Python
133 lines
5.8 KiB
Python
from abc import ABC, abstractmethod
|
|
from typing import Optional, Tuple
|
|
|
|
from sqlalchemy import String, and_, cast, func, select, text, update
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import aliased
|
|
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.database.model.models import items, quotations, sessions, suppliers, users
|
|
from common.enums import ErrorType
|
|
from common.logger import LOG
|
|
|
|
# 재협상 요청은 전용 테이블 없이 sessions.custom.renegotiation 에 들어간다(IMK #15).
|
|
# 조회는 세션을 견적·상품·공급사와 조인하면서 JSONB 조건으로 거른다.
|
|
_RENEGO = sessions.custom["renegotiation"]
|
|
|
|
|
|
class IRenegotiationCRUD(ABC):
|
|
@abstractmethod
|
|
async def list_requests(self, cdb: AsyncSession, company_id, status: Optional[int], skip: int, limit: int, owner_user_id=None) -> Tuple[ErrorType, list, int]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def get_request(self, cdb: AsyncSession, company_id, session_id) -> Tuple[ErrorType, Optional[tuple]]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def merge_custom(self, cdb: AsyncSession, session_id, patch: dict) -> ErrorType:
|
|
pass
|
|
|
|
|
|
class RenegotiationCRUD(IRenegotiationCRUD):
|
|
@staticmethod
|
|
def _base_query(company_id, status: Optional[int], owner_user_id=None):
|
|
# 회사 스코프는 견적 작성자(users.company_id)로 건다 — quotations 에 company_id 컬럼이 없다.
|
|
# owner_user_id 가 오면(일반관리자) 자기 견적만 — 본인 견적의 재협상 요청만 보고 처리한다. OWNER 는 None(회사 전체).
|
|
conds = [
|
|
sessions.deleted == False, # noqa: E712
|
|
users.company_id == company_id,
|
|
_RENEGO.isnot(None),
|
|
]
|
|
if owner_user_id is not None:
|
|
conds.append(quotations.user_id == owner_user_id)
|
|
if status is not None:
|
|
conds.append(cast(_RENEGO["status"].astext, String) == str(status))
|
|
return and_(*conds)
|
|
|
|
@staticmethod
|
|
def _select():
|
|
# 승인/반려한 담당자 이름 — custom.renegotiation.decided_by(user_id) 로 users 를 한 번 더(별칭) 조인.
|
|
decider = aliased(users)
|
|
return (
|
|
select(
|
|
sessions.session_id,
|
|
sessions.quotation_id,
|
|
sessions.supplier_id,
|
|
sessions.target_price,
|
|
sessions.bid_price,
|
|
sessions.custom,
|
|
quotations.number,
|
|
quotations.round,
|
|
quotations.name,
|
|
quotations.close_reason,
|
|
items.name,
|
|
suppliers.name,
|
|
decider.name,
|
|
users.user_id, # 견적 작성자(소유자) — 승인/반려 소유권 게이팅용 (row[13])
|
|
users.name, # 견적 담당자(작성자) 이름 — 리스트 표시용 (row[14])
|
|
sessions.item_id, # 상품 링크용 (row[15])
|
|
)
|
|
.select_from(sessions)
|
|
.join(quotations, quotations.qt_id == sessions.quotation_id)
|
|
.join(users, users.user_id == quotations.user_id)
|
|
.outerjoin(items, items.item_id == sessions.item_id)
|
|
.outerjoin(suppliers, suppliers.supplier_id == sessions.supplier_id)
|
|
.outerjoin(decider, cast(decider.user_id, String) == _RENEGO["decided_by"].astext)
|
|
)
|
|
|
|
async def list_requests(self, cdb: AsyncSession, company_id, status: Optional[int], skip: int, limit: int, owner_user_id=None) -> Tuple[ErrorType, list, int]:
|
|
try:
|
|
where = self._base_query(company_id, status, owner_user_id)
|
|
|
|
cnt_err, cnt_rows = await DB_SESSION_MNG.execute(
|
|
cdb,
|
|
select(func.count())
|
|
.select_from(sessions)
|
|
.join(quotations, quotations.qt_id == sessions.quotation_id)
|
|
.join(users, users.user_id == quotations.user_id)
|
|
.where(where),
|
|
)
|
|
if cnt_err != ErrorType.SUCCESS:
|
|
return cnt_err, [], 0
|
|
total = int(cnt_rows[0] or 0) if cnt_rows else 0
|
|
|
|
# 요청 시각 내림차순 — JSONB 텍스트지만 ISO8601 이라 사전순 = 시간순.
|
|
err, rows = await DB_SESSION_MNG.execute(
|
|
cdb,
|
|
self._select().where(where).order_by(_RENEGO["requested_at"].astext.desc()).offset(skip).limit(limit),
|
|
)
|
|
if err != ErrorType.SUCCESS:
|
|
return err, [], 0
|
|
return ErrorType.SUCCESS, list(rows), total
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, [], 0
|
|
|
|
async def get_request(self, cdb: AsyncSession, company_id, session_id) -> Tuple[ErrorType, Optional[tuple]]:
|
|
try:
|
|
err, rows = await DB_SESSION_MNG.execute(
|
|
cdb,
|
|
self._select().where(and_(self._base_query(company_id, None), sessions.session_id == session_id)).limit(1),
|
|
)
|
|
if err != ErrorType.SUCCESS:
|
|
return err, None
|
|
return ErrorType.SUCCESS, rows[0] if rows else None
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, None
|
|
|
|
async def merge_custom(self, cdb: AsyncSession, session_id, patch: dict) -> ErrorType:
|
|
# 부가정보와 같은 컬럼을 쓰므로 통째로 덮지 않고 병합한다.
|
|
try:
|
|
query = (
|
|
update(sessions)
|
|
.where(sessions.session_id == session_id)
|
|
.values(custom=func.coalesce(sessions.custom, cast(text("'{}'"), JSONB)).op("||")(cast(patch, JSONB)))
|
|
)
|
|
return await DB_SESSION_MNG.add(cdb, query)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED
|