"""Suppliers CRUD — v2.6: 1 supplier → exactly 1 category (category_id FK).""" from fastapi import APIRouter, Depends, Query from sqlalchemy import func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.auth import require_procurement_writer, require_role from app.db.engine import get_db from app.models.catalog import MaterialCategory from app.models.procurement import Quote, Supplier from app.schemas.procurement import SupplierCreate, SupplierUpdate from app.utils.errors import ConflictError, NotFoundError from app.utils.pagination import paginate, sort_query from app.utils.serialization import to_iso router = APIRouter(prefix="/suppliers", tags=["suppliers"]) _procurement_writer = require_procurement_writer() def supplier_to_dict(supplier: Supplier, category: MaterialCategory | None = None) -> dict: return { "id": str(supplier.id), "name": supplier.name, "category_id": str(supplier.category_id), "category": { "id": str(category.id), "slug": category.slug, "name": category.name, } if category else None, "location": supplier.location, "phone": supplier.phone, "contact_person": supplier.contact_person, "tax_id": supplier.tax_id, "created_at": to_iso(supplier.created_at), } async def _category_or_404(db: AsyncSession, category_id) -> MaterialCategory: category = await db.get(MaterialCategory, category_id) if category is None: raise NotFoundError("دسته‌بندی یافت نشد", field="category_id") return category @router.get("") async def list_suppliers( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), q: str | None = None, category_id=None, sort: str | None = None, db: AsyncSession = Depends(get_db), ): stmt = select(Supplier) if q: stmt = stmt.where( or_( Supplier.name.ilike(f"%{q}%"), Supplier.location.ilike(f"%{q}%"), Supplier.phone.ilike(f"%{q}%"), ) ) if category_id: stmt = stmt.where(Supplier.category_id == category_id) stmt = sort_query(stmt, Supplier, sort) items, meta = await paginate(stmt, page, page_size, db) cat_ids = {s.category_id for s in items} cats = {} if cat_ids: cats = { c.id: c for c in (await db.execute(select(MaterialCategory).where(MaterialCategory.id.in_(cat_ids)))).scalars() } return {"data": [supplier_to_dict(s, cats.get(s.category_id)) for s in items], "meta": meta} @router.post("", status_code=201) async def create_supplier( payload: SupplierCreate, db: AsyncSession = Depends(get_db), _auth=Depends(_procurement_writer), ): category = await _category_or_404(db, payload.category_id) supplier = Supplier( name=payload.name, category_id=payload.category_id, location=payload.location, phone=payload.phone, contact_person=payload.contact_person, tax_id=payload.tax_id, ) db.add(supplier) await db.flush() return {"data": supplier_to_dict(supplier, category)} @router.get("/{supplier_id}") async def get_supplier(supplier_id, db: AsyncSession = Depends(get_db)): supplier = await db.get(Supplier, supplier_id) if supplier is None: raise NotFoundError("تأمین‌کننده یافت نشد") category = await db.get(MaterialCategory, supplier.category_id) return {"data": supplier_to_dict(supplier, category)} @router.patch("/{supplier_id}") async def update_supplier( supplier_id, payload: SupplierUpdate, db: AsyncSession = Depends(get_db), _auth=Depends(_procurement_writer), ): supplier = await db.get(Supplier, supplier_id) if supplier is None: raise NotFoundError("تأمین‌کننده یافت نشد") if payload.name is not None: supplier.name = payload.name if payload.category_id is not None: if payload.category_id != supplier.category_id: await _category_or_404(db, payload.category_id) supplier.category_id = payload.category_id if payload.location is not None: supplier.location = payload.location if payload.phone is not None: supplier.phone = payload.phone if payload.contact_person is not None: supplier.contact_person = payload.contact_person if payload.tax_id is not None: supplier.tax_id = payload.tax_id await db.flush() category = await db.get(MaterialCategory, supplier.category_id) return {"data": supplier_to_dict(supplier, category)} @router.delete("/{supplier_id}") async def delete_supplier( supplier_id, db: AsyncSession = Depends(get_db), _auth=Depends(_procurement_writer), ): supplier = await db.get(Supplier, supplier_id) if supplier is None: raise NotFoundError("تأمین‌کننده یافت نشد") # quotes.supplier_id is NOT NULL RESTRICT — block if any quote exists. linked = await db.scalar(select(func.count()).select_from(Quote).where(Quote.supplier_id == supplier_id)) if linked: raise ConflictError("این تأمین‌کننده دارای پیشنهاد است و قابل حذف نیست") await db.delete(supplier) # supplier_materials cascade via DDL await db.flush() return {"data": {"id": str(supplier_id), "deleted": True}}