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

209 lines
7.5 KiB
Python
Raw Normal View History

"""EquipmentGroup (Bundle) 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, selectinload
from app.api.deps import get_current_user, require_permission
from app.db.session import get_async_session
from app.models import User, EquipmentGroup, Equipment, StockLocation
from app.models.equipment_group import equipment_group_items
from app.schemas.equipment_group import (
EquipmentGroupCreateRequest,
EquipmentGroupUpdateRequest,
EquipmentGroupResponse,
EquipmentGroupListResponse,
EquipmentGroupItem as EquipmentGroupItemSchema,
)
from app.schemas.equipment import LocationRef
router = APIRouter(prefix="/equipment-groups", tags=["equipment-groups"])
def _load_items(group: EquipmentGroup) -> list[dict]:
"""Build items list from M2M relationship."""
# The items relationship loads Equipment objects; we need quantity from the association
# We'll use a separate query approach in the endpoints.
return []
@router.get("", response_model=EquipmentGroupListResponse)
async def list_equipment_groups(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
search: str | None = Query(None, description="Search in name"),
current_user: User = Depends(require_permission("equipment:read")),
session: AsyncSession = Depends(get_async_session),
):
"""List equipment groups with search and pagination."""
account_id = current_user.account_id
base_q = select(EquipmentGroup).where(EquipmentGroup.account_id == account_id)
if search:
pattern = f"%{search}%"
base_q = base_q.where(EquipmentGroup.name.ilike(pattern))
count_q = select(func.count()).select_from(base_q.subquery())
total = (await session.execute(count_q)).scalar() or 0
q = (
base_q
.options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items))
.order_by(EquipmentGroup.name)
.offset((page - 1) * size)
.limit(size)
)
result = await session.execute(q)
groups = result.unique().scalars().all()
return EquipmentGroupListResponse(
items=[EquipmentGroupResponse.model_validate(g) for g in groups],
total=total,
page=page,
size=size,
)
@router.post("", response_model=EquipmentGroupResponse, status_code=status.HTTP_201_CREATED)
async def create_equipment_group(
body: EquipmentGroupCreateRequest,
current_user: User = Depends(require_permission("equipment:write")),
session: AsyncSession = Depends(get_async_session),
):
"""Create a new equipment group (bundle)."""
account_id = current_user.account_id
group = EquipmentGroup(
account_id=account_id,
name=body.name,
description=body.description,
daily_rate=body.daily_rate,
default_location_id=body.default_location_id,
)
if body.items:
equipment_ids = [i.equipment_id for i in body.items]
eq_result = await session.execute(
select(Equipment).where(
Equipment.id.in_(equipment_ids),
Equipment.account_id == account_id,
)
)
items = eq_result.scalars().all()
group.items = items
# Store quantities via the association table directly
for item_schema in body.items:
await session.execute(
equipment_group_items.update()
.where(
equipment_group_items.c.group_id == group.id,
equipment_group_items.c.equipment_id == item_schema.equipment_id,
)
.values(quantity=item_schema.quantity)
)
session.add(group)
await session.commit()
await session.refresh(group)
# Reload with relationships
await session.execute(
select(EquipmentGroup)
.options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items))
.where(EquipmentGroup.id == group.id)
)
await session.refresh(group)
return EquipmentGroupResponse.model_validate(group)
@router.get("/{group_id}", response_model=EquipmentGroupResponse)
async def get_equipment_group(
group_id: str,
current_user: User = Depends(require_permission("equipment:read")),
session: AsyncSession = Depends(get_async_session),
):
"""Get a specific equipment group with items."""
account_id = current_user.account_id
result = await session.execute(
select(EquipmentGroup)
.options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items))
.where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id)
)
group = result.unique().scalars().first()
if not group:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found")
return EquipmentGroupResponse.model_validate(group)
@router.put("/{group_id}", response_model=EquipmentGroupResponse)
async def update_equipment_group(
group_id: str,
body: EquipmentGroupUpdateRequest,
current_user: User = Depends(require_permission("equipment:write")),
session: AsyncSession = Depends(get_async_session),
):
"""Update an equipment group."""
account_id = current_user.account_id
result = await session.execute(
select(EquipmentGroup)
.options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items))
.where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id)
)
group = result.unique().scalars().first()
if not group:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found")
update_data = body.model_dump(exclude_unset=True, exclude={"items"})
for field, value in update_data.items():
setattr(group, field, value)
# Handle items replacement
if body.items is not None:
equipment_ids = [i.equipment_id for i in body.items]
eq_result = await session.execute(
select(Equipment).where(
Equipment.id.in_(equipment_ids),
Equipment.account_id == account_id,
)
)
group.items = eq_result.scalars().all()
# Update quantities
for item_schema in body.items:
await session.execute(
equipment_group_items.update()
.where(
equipment_group_items.c.group_id == group.id,
equipment_group_items.c.equipment_id == item_schema.equipment_id,
)
.values(quantity=item_schema.quantity)
)
await session.commit()
await session.refresh(group)
return EquipmentGroupResponse.model_validate(group)
@router.delete("/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_equipment_group(
group_id: str,
current_user: User = Depends(require_permission("equipment:delete")),
session: AsyncSession = Depends(get_async_session),
):
"""Delete an equipment group."""
account_id = current_user.account_id
result = await session.execute(
select(EquipmentGroup).where(
EquipmentGroup.id == group_id,
EquipmentGroup.account_id == account_id,
)
)
group = result.scalars().first()
if not group:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found")
await session.delete(group)
await session.commit()
return None