o2o-negosium-original/negodata/backend/crud/renegotiation_crud.py

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