Files
ERP/backend/app/routers/item_receipts.py
T
MMD 0ea0322967 Revert 253c922: restore the PM/RBAC work to the integration
Reverting the production revert so the pm branch can merge cleanly on
top of the restored RBAC base. Conflicts against newer integration work
resolved by combining:
- App.tsx: v7's bot-draft wizard bypass + login returnUrl kept alongside
  the supervisor gating (no-project block screen, panel scoping)
- LoginPage: returnUrl navigation kept, supervisor routing added
- migration.sql: bot_drafts table kept, unified-projects ALTERs restored
2026-10-01 15:24:56 +03:30

100 lines
4.0 KiB
Python

"""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.auth import require_role
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"],
)
_procurement_writer = require_role("admin", "procurement_manager")
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),
_auth=Depends(_procurement_writer),
):
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),
_auth=Depends(_procurement_writer),
):
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)}