Files
home_stock/backend/app/routers/items.py
2026-01-28 19:22:30 +01:00

265 lines
8.4 KiB
Python

"""Router API pour les objets d'inventaire."""
from decimal import Decimal
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.models.item import ItemStatus
from app.repositories.category import CategoryRepository
from app.repositories.item import ItemRepository
from app.repositories.location import LocationRepository
from app.schemas.common import PaginatedResponse, SuccessResponse
from app.schemas.item import (
ItemCreate,
ItemResponse,
ItemUpdate,
ItemWithRelations,
)
router = APIRouter(prefix="/items", tags=["Items"])
@router.get("", response_model=PaginatedResponse[ItemWithRelations])
async def list_items(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
search: str | None = Query(default=None, min_length=2),
category_id: int | None = None,
location_id: int | None = None,
status: ItemStatus | None = None,
min_price: Decimal | None = None,
max_price: Decimal | None = None,
db: AsyncSession = Depends(get_db),
) -> PaginatedResponse[ItemWithRelations]:
"""Liste les objets avec filtres et pagination."""
repo = ItemRepository(db)
skip = (page - 1) * page_size
items = await repo.search(
query=search or "",
category_id=category_id,
location_id=location_id,
status=status,
min_price=min_price,
max_price=max_price,
skip=skip,
limit=page_size,
)
total = await repo.count_filtered(
query=search,
category_id=category_id,
location_id=location_id,
status=status,
min_price=min_price,
max_price=max_price,
)
result_items = [ItemWithRelations.model_validate(item) for item in items]
return PaginatedResponse.create(items=result_items, total=total, page=page, page_size=page_size)
@router.get("/{item_id}", response_model=ItemWithRelations)
async def get_item(
item_id: int,
db: AsyncSession = Depends(get_db),
) -> ItemWithRelations:
"""Récupère un objet par son ID avec ses relations."""
repo = ItemRepository(db)
item = await repo.get_with_relations(item_id)
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Objet {item_id} non trouvé",
)
return ItemWithRelations.model_validate(item)
@router.post("", response_model=ItemResponse, status_code=status.HTTP_201_CREATED)
async def create_item(
data: ItemCreate,
db: AsyncSession = Depends(get_db),
) -> ItemResponse:
"""Crée un nouvel objet."""
item_repo = ItemRepository(db)
category_repo = CategoryRepository(db)
location_repo = LocationRepository(db)
# Vérifier que la catégorie existe
if not await category_repo.exists(data.category_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Catégorie {data.category_id} non trouvée",
)
# Vérifier que l'emplacement existe
if not await location_repo.exists(data.location_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Emplacement {data.location_id} non trouvé",
)
# Vérifier l'unicité du numéro de série si fourni
if data.serial_number:
existing = await item_repo.get_by_serial_number(data.serial_number)
if existing:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Un objet avec le numéro de série '{data.serial_number}' existe déjà",
)
item = await item_repo.create(
name=data.name,
description=data.description,
quantity=data.quantity,
status=data.status,
brand=data.brand,
model=data.model,
serial_number=data.serial_number,
price=data.price,
purchase_date=data.purchase_date,
notes=data.notes,
category_id=data.category_id,
location_id=data.location_id,
)
await db.commit()
return ItemResponse.model_validate(item)
@router.put("/{item_id}", response_model=ItemResponse)
async def update_item(
item_id: int,
data: ItemUpdate,
db: AsyncSession = Depends(get_db),
) -> ItemResponse:
"""Met à jour un objet."""
item_repo = ItemRepository(db)
category_repo = CategoryRepository(db)
location_repo = LocationRepository(db)
# Vérifier que l'objet existe
existing = await item_repo.get(item_id)
if existing is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Objet {item_id} non trouvé",
)
# Vérifier la catégorie si changée
if data.category_id is not None and data.category_id != existing.category_id:
if not await category_repo.exists(data.category_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Catégorie {data.category_id} non trouvée",
)
# Vérifier l'emplacement si changé
if data.location_id is not None and data.location_id != existing.location_id:
if not await location_repo.exists(data.location_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Emplacement {data.location_id} non trouvé",
)
# Vérifier l'unicité du numéro de série si changé
if data.serial_number and data.serial_number != existing.serial_number:
existing_with_serial = await item_repo.get_by_serial_number(data.serial_number)
if existing_with_serial and existing_with_serial.id != item_id:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Un objet avec le numéro de série '{data.serial_number}' existe déjà",
)
item = await item_repo.update(
item_id,
name=data.name,
description=data.description,
quantity=data.quantity,
status=data.status,
brand=data.brand,
model=data.model,
serial_number=data.serial_number,
price=data.price,
purchase_date=data.purchase_date,
notes=data.notes,
category_id=data.category_id,
location_id=data.location_id,
)
await db.commit()
return ItemResponse.model_validate(item)
@router.delete("/{item_id}", response_model=SuccessResponse)
async def delete_item(
item_id: int,
db: AsyncSession = Depends(get_db),
) -> SuccessResponse:
"""Supprime un objet et ses documents associés."""
repo = ItemRepository(db)
# Vérifier que l'objet existe
if not await repo.exists(item_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Objet {item_id} non trouvé",
)
await repo.delete(item_id)
await db.commit()
return SuccessResponse(message="Objet supprimé avec succès", id=item_id)
@router.patch("/{item_id}/status", response_model=ItemResponse)
async def update_item_status(
item_id: int,
new_status: ItemStatus,
db: AsyncSession = Depends(get_db),
) -> ItemResponse:
"""Met à jour le statut d'un objet."""
repo = ItemRepository(db)
item = await repo.update(item_id, status=new_status)
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Objet {item_id} non trouvé",
)
await db.commit()
return ItemResponse.model_validate(item)
@router.patch("/{item_id}/location", response_model=ItemResponse)
async def move_item(
item_id: int,
new_location_id: int,
db: AsyncSession = Depends(get_db),
) -> ItemResponse:
"""Déplace un objet vers un nouvel emplacement."""
item_repo = ItemRepository(db)
location_repo = LocationRepository(db)
# Vérifier que le nouvel emplacement existe
if not await location_repo.exists(new_location_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Emplacement {new_location_id} non trouvé",
)
item = await item_repo.update(item_id, location_id=new_location_id)
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Objet {item_id} non trouvé",
)
await db.commit()
return ItemResponse.model_validate(item)