"""Item receipts — 1:1 with item_decisions. Quote-level arrival uses separate endpoints.""" from fastapi import APIRouter, Depends from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db.engine import get_db from app.models.procurement import ItemDecision, ItemReceipt, LineItem from app.schemas.procurement import ReceiptCreate, ReceiptUpdate from app.services.expand_service import receipt_full from app.services.quote_service import _fallback_confirmer_id from app.utils.errors import ConflictError, NotFoundError router = APIRouter( prefix="/quotes/{quote_id}/line-items/{line_item_id}/receipt", tags=["item-receipts"], ) async def _line_item_or_404(db: AsyncSession, quote_id, line_item_id) -> LineItem: li = await db.scalar( select(LineItem).where(LineItem.id == line_item_id, LineItem.quote_id == quote_id) ) if li is None: raise NotFoundError("خط آیتم یافت نشد") return li async def _decision_or_404(db: AsyncSession, line_item_id) -> ItemDecision: decision = await db.scalar(select(ItemDecision).where(ItemDecision.line_item_id == line_item_id)) if decision is None: raise NotFoundError("تصمیمی برای این خط آیتم ثبت نشده است") return decision @router.get("") async def get_receipt(quote_id, line_item_id, db: AsyncSession = Depends(get_db)): await _line_item_or_404(db, quote_id, line_item_id) decision = await db.scalar(select(ItemDecision).where(ItemDecision.line_item_id == line_item_id)) if decision is None: raise NotFoundError("رسیدی برای این خط آیتم ثبت نشده است") receipt = await db.scalar(select(ItemReceipt).where(ItemReceipt.item_decision_id == decision.id)) if receipt is None: raise NotFoundError("رسیدی برای این خط آیتم ثبت نشده است") return {"data": receipt_full(receipt)} @router.post("", status_code=201) async def create_receipt(quote_id, line_item_id, payload: ReceiptCreate, db: AsyncSession = Depends(get_db)): await _line_item_or_404(db, quote_id, line_item_id) decision = await _decision_or_404(db, line_item_id) existing = await db.scalar(select(ItemReceipt).where(ItemReceipt.item_decision_id == decision.id)) if existing is not None: raise ConflictError("رسید این خط آیتم قبلاً ثبت شده است") receipt = ItemReceipt( item_decision_id=decision.id, confirmed_by=await _fallback_confirmer_id(db), received_at=payload.received_at, received_qty=payload.received_qty, on_time=payload.on_time, quality_note=payload.quality_note, ) db.add(receipt) await db.flush() return {"data": receipt_full(receipt)} @router.patch("") async def update_receipt(quote_id, line_item_id, payload: ReceiptUpdate, db: AsyncSession = Depends(get_db)): await _line_item_or_404(db, quote_id, line_item_id) decision = await db.scalar(select(ItemDecision).where(ItemDecision.line_item_id == line_item_id)) if decision is None: raise NotFoundError("تصمیمی برای این خط آیتم ثبت نشده است") receipt = await db.scalar(select(ItemReceipt).where(ItemReceipt.item_decision_id == decision.id)) if receipt is None: raise NotFoundError("رسیدی برای این خط آیتم ثبت نشده است") if payload.received_at is not None: receipt.received_at = payload.received_at if payload.received_qty is not None: receipt.received_qty = payload.received_qty if payload.on_time is not None: receipt.on_time = payload.on_time if payload.quality_note is not None: receipt.quality_note = payload.quality_note await db.flush() return {"data": receipt_full(receipt)}