209 lines
7.5 KiB
Python
209 lines
7.5 KiB
Python
|
|
"""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
|