Files
bakery-ia/services/inventory/app/api/stock.py

317 lines
11 KiB
Python
Raw Normal View History

# services/inventory/app/api/stock.py
"""
API endpoints for stock management
"""
from typing import List, Optional
from uuid import UUID
2025-09-09 21:39:12 +02:00
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.services.inventory_service import InventoryService
from app.schemas.inventory import (
StockCreate,
StockUpdate,
StockResponse,
StockMovementCreate,
StockMovementResponse,
StockFilter
)
from shared.auth.decorators import get_current_user_dep
2025-09-09 21:39:12 +02:00
router = APIRouter(tags=["stock"])
# Helper function to extract user ID from user object
def get_current_user_id(current_user: dict = Depends(get_current_user_dep)) -> UUID:
"""Extract user ID from current user context"""
user_id = current_user.get('user_id')
if not user_id:
2025-09-09 21:39:12 +02:00
# Handle service tokens that don't have UUID user_ids
if current_user.get('type') == 'service':
return None
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User ID not found in context"
)
2025-09-09 21:39:12 +02:00
try:
return UUID(user_id)
except (ValueError, TypeError):
return None
2025-09-09 21:39:12 +02:00
@router.post("/tenants/{tenant_id}/stock", response_model=StockResponse)
async def add_stock(
stock_data: StockCreate,
2025-09-09 21:39:12 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Add new stock entry"""
try:
2025-09-09 21:39:12 +02:00
# Extract user ID - handle service tokens
user_id = get_current_user_id(current_user)
service = InventoryService()
stock = await service.add_stock(stock_data, tenant_id, user_id)
return stock
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to add stock"
)
2025-09-09 21:39:12 +02:00
@router.post("/tenants/{tenant_id}/stock/consume")
async def consume_stock(
2025-09-09 21:39:12 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
ingredient_id: UUID = Query(..., description="Ingredient ID to consume"),
quantity: float = Query(..., gt=0, description="Quantity to consume"),
reference_number: Optional[str] = Query(None, description="Reference number (e.g., production order)"),
notes: Optional[str] = Query(None, description="Additional notes"),
fifo: bool = Query(True, description="Use FIFO (First In, First Out) method"),
2025-09-09 21:39:12 +02:00
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Consume stock for production"""
try:
2025-09-09 21:39:12 +02:00
# Extract user ID - handle service tokens
user_id = get_current_user_id(current_user)
service = InventoryService()
consumed_items = await service.consume_stock(
ingredient_id, quantity, tenant_id, user_id, reference_number, notes, fifo
)
return {
"ingredient_id": str(ingredient_id),
"total_quantity_consumed": quantity,
"consumed_items": consumed_items,
"method": "FIFO" if fifo else "LIFO"
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to consume stock"
)
2025-09-09 21:39:12 +02:00
@router.get("/tenants/{tenant_id}/stock/expiring", response_model=List[dict])
async def get_expiring_stock(
2025-09-09 21:39:12 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
days_ahead: int = Query(7, ge=1, le=365, description="Days ahead to check for expiring items"),
db: AsyncSession = Depends(get_db)
):
"""Get stock items expiring within specified days"""
try:
service = InventoryService()
expiring_items = await service.check_expiration_alerts(tenant_id, days_ahead)
return expiring_items
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get expiring stock"
)
2025-09-09 21:39:12 +02:00
@router.get("/tenants/{tenant_id}/stock/low-stock", response_model=List[dict])
async def get_low_stock(
2025-09-09 21:39:12 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get ingredients with low stock levels"""
try:
service = InventoryService()
low_stock_items = await service.check_low_stock_alerts(tenant_id)
return low_stock_items
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get low stock items"
)
2025-09-09 21:39:12 +02:00
@router.get("/tenants/{tenant_id}/stock/summary", response_model=dict)
async def get_stock_summary(
2025-09-09 21:39:12 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get stock summary for dashboard"""
try:
service = InventoryService()
summary = await service.get_inventory_summary(tenant_id)
return summary.dict()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get stock summary"
2025-09-09 21:39:12 +02:00
)
@router.get("/tenants/{tenant_id}/stock", response_model=List[StockResponse])
async def get_stock(
tenant_id: UUID = Path(..., description="Tenant ID"),
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of records to return"),
ingredient_id: Optional[UUID] = Query(None, description="Filter by ingredient"),
available_only: bool = Query(True, description="Show only available stock"),
db: AsyncSession = Depends(get_db)
):
"""Get stock entries with filtering"""
try:
service = InventoryService()
stock_entries = await service.get_stock(
tenant_id, skip, limit, ingredient_id, available_only
)
return stock_entries
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get stock entries"
)
@router.get("/tenants/{tenant_id}/stock/{stock_id}", response_model=StockResponse)
async def get_stock_entry(
stock_id: UUID = Path(..., description="Stock entry ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get specific stock entry"""
try:
service = InventoryService()
stock = await service.get_stock_entry(stock_id, tenant_id)
if not stock:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Stock entry not found"
)
return stock
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get stock entry"
)
@router.put("/tenants/{tenant_id}/stock/{stock_id}", response_model=StockResponse)
async def update_stock(
2025-09-09 22:27:52 +02:00
stock_data: StockUpdate,
2025-09-09 21:39:12 +02:00
stock_id: UUID = Path(..., description="Stock entry ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Update stock entry"""
try:
service = InventoryService()
stock = await service.update_stock(stock_id, stock_data, tenant_id)
if not stock:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Stock entry not found"
)
return stock
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update stock entry"
)
@router.delete("/tenants/{tenant_id}/stock/{stock_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_stock(
stock_id: UUID = Path(..., description="Stock entry ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Delete stock entry (mark as unavailable)"""
try:
service = InventoryService()
deleted = await service.delete_stock(stock_id, tenant_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Stock entry not found"
)
return None
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete stock entry"
)
@router.post("/tenants/{tenant_id}/stock/movements", response_model=StockMovementResponse)
async def create_stock_movement(
movement_data: StockMovementCreate,
2025-09-09 22:27:52 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
2025-09-09 21:39:12 +02:00
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create stock movement record"""
try:
# Extract user ID - handle service tokens
user_id = get_current_user_id(current_user)
service = InventoryService()
movement = await service.create_stock_movement(movement_data, tenant_id, user_id)
return movement
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create stock movement"
)
@router.get("/tenants/{tenant_id}/stock/movements", response_model=List[StockMovementResponse])
async def get_stock_movements(
tenant_id: UUID = Path(..., description="Tenant ID"),
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of records to return"),
ingredient_id: Optional[UUID] = Query(None, description="Filter by ingredient"),
movement_type: Optional[str] = Query(None, description="Filter by movement type"),
db: AsyncSession = Depends(get_db)
):
"""Get stock movements with filtering"""
try:
service = InventoryService()
movements = await service.get_stock_movements(
tenant_id, skip, limit, ingredient_id, movement_type
)
return movements
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get stock movements"
)