Files
rentman-clone/backend/app/api/v1/equipment.py
T

201 lines
6.6 KiB
Python
Raw Normal View History

"""Equipment CRUD endpoints."""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from app.api.deps import require_permission
from app.db.session import get_async_session
from app.models import User, Equipment, StockLocation, Contact
from app.schemas.equipment import (
EquipmentCreateRequest,
EquipmentUpdateRequest,
EquipmentResponse,
EquipmentListResponse,
)
router = APIRouter(prefix="/equipment", tags=["equipment"])
@router.get("", response_model=EquipmentListResponse)
async def list_equipment(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
search: str | None = Query(
None, description="Search in name, brand, serial_number"
),
category: str | None = Query(None, description="Filter by category"),
status: str | None = Query(None, description="Filter by status"),
location_id: str | None = Query(None, description="Filter by location"),
current_user: User = Depends(require_permission("equipment:read")),
session: AsyncSession = Depends(get_async_session),
):
"""List equipment with search, filters, pagination."""
account_id = current_user.account_id
base_q = select(Equipment).where(Equipment.account_id == account_id)
if search:
pattern = f"%{search}%"
base_q = base_q.where(
or_(
Equipment.name.ilike(pattern),
Equipment.brand.ilike(pattern),
Equipment.serial_number.ilike(pattern),
Equipment.barcode.ilike(pattern),
)
)
if category:
base_q = base_q.where(Equipment.category == category)
if status:
base_q = base_q.where(Equipment.status == status)
if location_id:
base_q = base_q.where(Equipment.location_id == location_id)
count_q = select(func.count()).select_from(base_q.subquery())
total = (await session.execute(count_q)).scalar() or 0
q = (
base_q.options(joinedload(Equipment.location), joinedload(Equipment.supplier))
.order_by(Equipment.updated_at.desc())
.offset((page - 1) * size)
.limit(size)
)
result = await session.execute(q)
items = result.unique().scalars().all()
return EquipmentListResponse(
items=[EquipmentResponse.model_validate(i) for i in items],
total=total,
page=page,
size=size,
)
@router.post("", response_model=EquipmentResponse, status_code=status.HTTP_201_CREATED)
async def create_equipment(
body: EquipmentCreateRequest,
current_user: User = Depends(require_permission("equipment:write")),
session: AsyncSession = Depends(get_async_session),
):
"""Create a new equipment item."""
account_id = current_user.account_id
# Validate location if provided
if body.location_id:
loc_result = await session.execute(
select(StockLocation).where(
StockLocation.id == body.location_id,
StockLocation.account_id == account_id,
)
)
if not loc_result.scalars().first():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Stock location not found",
)
# Validate supplier if provided
if body.supplier_id:
sup_result = await session.execute(
select(Contact).where(
Contact.id == body.supplier_id,
Contact.account_id == account_id,
)
)
if not sup_result.scalars().first():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Supplier (contact) not found",
)
item = Equipment(account_id=account_id, **body.model_dump())
session.add(item)
await session.commit()
await session.refresh(item)
await session.execute(
select(Equipment)
.options(joinedload(Equipment.location), joinedload(Equipment.supplier))
.where(Equipment.id == item.id)
)
await session.refresh(item)
return EquipmentResponse.model_validate(item)
@router.get("/{item_id}", response_model=EquipmentResponse)
async def get_equipment(
item_id: str,
current_user: User = Depends(require_permission("equipment:read")),
session: AsyncSession = Depends(get_async_session),
):
"""Get a specific equipment item."""
account_id = current_user.account_id
result = await session.execute(
select(Equipment)
.options(joinedload(Equipment.location), joinedload(Equipment.supplier))
.where(Equipment.id == item_id, Equipment.account_id == account_id)
)
item = result.unique().scalars().first()
if not item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found"
)
return EquipmentResponse.model_validate(item)
@router.put("/{item_id}", response_model=EquipmentResponse)
async def update_equipment(
item_id: str,
body: EquipmentUpdateRequest,
current_user: User = Depends(require_permission("equipment:write")),
session: AsyncSession = Depends(get_async_session),
):
"""Update an existing equipment item."""
account_id = current_user.account_id
result = await session.execute(
select(Equipment)
.options(joinedload(Equipment.location), joinedload(Equipment.supplier))
.where(Equipment.id == item_id, Equipment.account_id == account_id)
)
item = result.unique().scalars().first()
if not item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found"
)
update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(item, field, value)
await session.commit()
await session.refresh(item)
return EquipmentResponse.model_validate(item)
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_equipment(
item_id: str,
current_user: User = Depends(require_permission("equipment:delete")),
session: AsyncSession = Depends(get_async_session),
):
"""Delete an equipment item."""
account_id = current_user.account_id
result = await session.execute(
select(Equipment).where(
Equipment.id == item_id, Equipment.account_id == account_id
)
)
item = result.scalars().first()
if not item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found"
)
await session.delete(item)
await session.commit()
return None