1470 lines
60 KiB
Python
1470 lines
60 KiB
Python
# ================================================================
|
|
# services/production/app/api/production.py
|
|
# ================================================================
|
|
"""
|
|
Production API endpoints
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
|
from typing import Optional, List
|
|
from datetime import date, datetime, timedelta
|
|
from uuid import UUID
|
|
import structlog
|
|
|
|
from shared.auth.decorators import get_current_user_dep
|
|
from app.core.database import get_db
|
|
from app.services.production_service import ProductionService
|
|
from app.schemas.production import (
|
|
ProductionBatchCreate, ProductionBatchUpdate, ProductionBatchStatusUpdate,
|
|
ProductionBatchResponse, ProductionBatchListResponse,
|
|
ProductionScheduleCreate, ProductionScheduleUpdate, ProductionScheduleResponse,
|
|
DailyProductionRequirements, ProductionDashboardSummary, ProductionMetrics,
|
|
ProductionStatusEnum
|
|
)
|
|
from app.core.config import settings
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
router = APIRouter(tags=["production"])
|
|
|
|
|
|
def get_production_service() -> ProductionService:
|
|
"""Dependency injection for production service"""
|
|
from app.core.database import database_manager
|
|
return ProductionService(database_manager, settings)
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
# DASHBOARD ENDPOINTS
|
|
# ================================================================
|
|
|
|
@router.get("/tenants/{tenant_id}/production/dashboard/summary", response_model=ProductionDashboardSummary)
|
|
async def get_dashboard_summary(
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Get production dashboard summary using shared auth"""
|
|
try:
|
|
|
|
summary = await production_service.get_dashboard_summary(tenant_id)
|
|
|
|
logger.info("Retrieved production dashboard summary",
|
|
tenant_id=str(tenant_id))
|
|
|
|
return summary
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting daily production requirements",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get daily requirements")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/requirements", response_model=dict)
|
|
async def get_production_requirements(
|
|
tenant_id: UUID = Path(...),
|
|
date: Optional[date] = Query(None, description="Target date for production requirements"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Get production requirements for procurement planning"""
|
|
try:
|
|
|
|
target_date = date or datetime.now().date()
|
|
requirements = await production_service.get_production_requirements(tenant_id, target_date)
|
|
|
|
logger.info("Retrieved production requirements for procurement",
|
|
tenant_id=str(tenant_id), date=target_date.isoformat())
|
|
|
|
return requirements
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting production requirements",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get production requirements")
|
|
|
|
|
|
# ================================================================
|
|
# PRODUCTION BATCH ENDPOINTS
|
|
# ================================================================
|
|
|
|
@router.get("/tenants/{tenant_id}/production/batches", response_model=ProductionBatchListResponse)
|
|
async def list_production_batches(
|
|
tenant_id: UUID = Path(...),
|
|
status: Optional[ProductionStatusEnum] = Query(None, description="Filter by status"),
|
|
product_id: Optional[UUID] = Query(None, description="Filter by product"),
|
|
order_id: Optional[UUID] = Query(None, description="Filter by order"),
|
|
start_date: Optional[date] = Query(None, description="Filter from date"),
|
|
end_date: Optional[date] = Query(None, description="Filter to date"),
|
|
page: int = Query(1, ge=1, description="Page number"),
|
|
page_size: int = Query(50, ge=1, le=100, description="Page size"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""List batches with filters: date, status, product, order_id"""
|
|
try:
|
|
|
|
filters = {
|
|
"status": status,
|
|
"product_id": str(product_id) if product_id else None,
|
|
"order_id": str(order_id) if order_id else None,
|
|
"start_date": start_date,
|
|
"end_date": end_date
|
|
}
|
|
|
|
batch_list = await production_service.get_production_batches_list(tenant_id, filters, page, page_size)
|
|
|
|
logger.info("Retrieved production batches list",
|
|
tenant_id=str(tenant_id), filters=filters)
|
|
|
|
return batch_list
|
|
|
|
except Exception as e:
|
|
logger.error("Error listing production batches",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to list production batches")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/batches", response_model=ProductionBatchResponse)
|
|
async def create_production_batch(
|
|
batch_data: ProductionBatchCreate,
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Create a new production batch"""
|
|
try:
|
|
|
|
batch = await production_service.create_production_batch(tenant_id, batch_data)
|
|
|
|
logger.info("Created production batch",
|
|
batch_id=str(batch.id), tenant_id=str(tenant_id))
|
|
|
|
return ProductionBatchResponse.model_validate(batch)
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid batch data", error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error creating production batch",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to create production batch")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/batches/active", response_model=ProductionBatchListResponse)
|
|
async def get_active_batches(
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Get currently active production batches"""
|
|
try:
|
|
|
|
from app.repositories.production_batch_repository import ProductionBatchRepository
|
|
batch_repo = ProductionBatchRepository(db)
|
|
|
|
batches = await batch_repo.get_active_batches(str(tenant_id))
|
|
batch_responses = [ProductionBatchResponse.model_validate(batch) for batch in batches]
|
|
|
|
logger.info("Retrieved active production batches",
|
|
count=len(batches), tenant_id=str(tenant_id))
|
|
|
|
return ProductionBatchListResponse(
|
|
batches=batch_responses,
|
|
total_count=len(batches),
|
|
page=1,
|
|
page_size=len(batches)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting active batches",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get active batches")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/batches/{batch_id}", response_model=ProductionBatchResponse)
|
|
async def get_batch_details(
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Get detailed information about a production batch"""
|
|
try:
|
|
|
|
from app.repositories.production_batch_repository import ProductionBatchRepository
|
|
batch_repo = ProductionBatchRepository(db)
|
|
|
|
batch = await batch_repo.get(batch_id)
|
|
if not batch or str(batch.tenant_id) != str(tenant_id):
|
|
raise HTTPException(status_code=404, detail="Production batch not found")
|
|
|
|
logger.info("Retrieved production batch details",
|
|
batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
|
|
return ProductionBatchResponse.model_validate(batch)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error getting batch details",
|
|
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get batch details")
|
|
|
|
|
|
@router.put("/tenants/{tenant_id}/production/batches/{batch_id}/status", response_model=ProductionBatchResponse)
|
|
async def update_batch_status(
|
|
status_update: ProductionBatchStatusUpdate,
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Update production batch status"""
|
|
try:
|
|
|
|
batch = await production_service.update_batch_status(tenant_id, batch_id, status_update)
|
|
|
|
logger.info("Updated production batch status",
|
|
batch_id=str(batch_id),
|
|
new_status=status_update.status.value,
|
|
tenant_id=str(tenant_id))
|
|
|
|
return ProductionBatchResponse.model_validate(batch)
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid status update", error=str(e), batch_id=str(batch_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error updating batch status",
|
|
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to update batch status")
|
|
|
|
|
|
@router.put("/tenants/{tenant_id}/production/batches/{batch_id}", response_model=ProductionBatchResponse)
|
|
async def update_production_batch(
|
|
batch_update: ProductionBatchUpdate,
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Update batch (e.g., start time, notes, status)"""
|
|
try:
|
|
|
|
batch = await production_service.update_production_batch(tenant_id, batch_id, batch_update)
|
|
|
|
logger.info("Updated production batch",
|
|
batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
|
|
return ProductionBatchResponse.model_validate(batch)
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid batch update", error=str(e), batch_id=str(batch_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error updating production batch",
|
|
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to update production batch")
|
|
|
|
|
|
@router.delete("/tenants/{tenant_id}/production/batches/{batch_id}")
|
|
async def delete_production_batch(
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Cancel/delete draft batch (soft delete preferred)"""
|
|
try:
|
|
|
|
await production_service.delete_production_batch(tenant_id, batch_id)
|
|
|
|
logger.info("Deleted production batch",
|
|
batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
|
|
return {"message": "Production batch deleted successfully"}
|
|
|
|
except ValueError as e:
|
|
logger.warning("Cannot delete batch", error=str(e), batch_id=str(batch_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error deleting production batch",
|
|
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to delete production batch")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/batches/{batch_id}/start", response_model=ProductionBatchResponse)
|
|
async def start_production_batch(
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Mark batch as started (updates actual_start_time)"""
|
|
try:
|
|
|
|
batch = await production_service.start_production_batch(tenant_id, batch_id)
|
|
|
|
logger.info("Started production batch",
|
|
batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
|
|
return ProductionBatchResponse.model_validate(batch)
|
|
|
|
except ValueError as e:
|
|
logger.warning("Cannot start batch", error=str(e), batch_id=str(batch_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error starting production batch",
|
|
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to start production batch")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/batches/{batch_id}/complete", response_model=ProductionBatchResponse)
|
|
async def complete_production_batch(
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: UUID = Path(...),
|
|
completion_data: Optional[dict] = None,
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Complete batch — auto-calculates yield, duration, cost summary"""
|
|
try:
|
|
|
|
batch = await production_service.complete_production_batch(tenant_id, batch_id, completion_data)
|
|
|
|
logger.info("Completed production batch",
|
|
batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
|
|
return ProductionBatchResponse.model_validate(batch)
|
|
|
|
except ValueError as e:
|
|
logger.warning("Cannot complete batch", error=str(e), batch_id=str(batch_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error completing production batch",
|
|
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to complete production batch")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/batches/stats", response_model=dict)
|
|
async def get_production_batch_stats(
|
|
tenant_id: UUID = Path(...),
|
|
start_date: Optional[date] = Query(None, description="Start date for stats"),
|
|
end_date: Optional[date] = Query(None, description="End date for stats"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Aggregated stats: completed vs failed, avg yield, on-time rate"""
|
|
try:
|
|
|
|
# Default to last 30 days if no dates provided
|
|
if not start_date:
|
|
start_date = (datetime.now() - timedelta(days=30)).date()
|
|
if not end_date:
|
|
end_date = datetime.now().date()
|
|
|
|
stats = await production_service.get_batch_statistics(tenant_id, start_date, end_date)
|
|
|
|
logger.info("Retrieved production batch statistics",
|
|
tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat())
|
|
|
|
return stats
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting production batch stats",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get production batch stats")
|
|
|
|
|
|
# ================================================================
|
|
# PRODUCTION SCHEDULE ENDPOINTS
|
|
# ================================================================
|
|
|
|
@router.get("/tenants/{tenant_id}/production/schedules", response_model=dict)
|
|
async def get_production_schedule(
|
|
tenant_id: UUID = Path(...),
|
|
start_date: Optional[date] = Query(None, description="Start date for schedule"),
|
|
end_date: Optional[date] = Query(None, description="End date for schedule"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Get production schedule for a date range"""
|
|
try:
|
|
|
|
# Default to next 7 days if no dates provided
|
|
if not start_date:
|
|
start_date = datetime.now().date()
|
|
if not end_date:
|
|
end_date = start_date + timedelta(days=7)
|
|
|
|
from app.repositories.production_schedule_repository import ProductionScheduleRepository
|
|
schedule_repo = ProductionScheduleRepository(db)
|
|
|
|
schedules = await schedule_repo.get_schedules_by_date_range(
|
|
str(tenant_id), start_date, end_date
|
|
)
|
|
|
|
schedule_data = {
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"schedules": [
|
|
{
|
|
"id": str(schedule.id),
|
|
"date": schedule.schedule_date.isoformat(),
|
|
"shift_start": schedule.shift_start.isoformat(),
|
|
"shift_end": schedule.shift_end.isoformat(),
|
|
"capacity_utilization": schedule.utilization_percentage,
|
|
"batches_planned": schedule.total_batches_planned,
|
|
"is_finalized": schedule.is_finalized
|
|
}
|
|
for schedule in schedules
|
|
],
|
|
"total_schedules": len(schedules)
|
|
}
|
|
|
|
logger.info("Retrieved production schedule",
|
|
tenant_id=str(tenant_id),
|
|
start_date=start_date.isoformat(),
|
|
end_date=end_date.isoformat(),
|
|
schedules_count=len(schedules))
|
|
|
|
return schedule_data
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting production schedule",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get production schedule")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/schedules/{schedule_id}", response_model=ProductionScheduleResponse)
|
|
async def get_production_schedule_details(
|
|
tenant_id: UUID = Path(...),
|
|
schedule_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Retrieve full schedule details including assignments"""
|
|
try:
|
|
|
|
from app.repositories.production_schedule_repository import ProductionScheduleRepository
|
|
schedule_repo = ProductionScheduleRepository(db)
|
|
|
|
schedule = await schedule_repo.get(schedule_id)
|
|
if not schedule or str(schedule.tenant_id) != str(tenant_id):
|
|
raise HTTPException(status_code=404, detail="Production schedule not found")
|
|
|
|
logger.info("Retrieved production schedule details",
|
|
schedule_id=str(schedule_id), tenant_id=str(tenant_id))
|
|
|
|
return ProductionScheduleResponse.model_validate(schedule)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error getting production schedule details",
|
|
error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get production schedule details")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/schedules", response_model=ProductionScheduleResponse)
|
|
async def create_production_schedule(
|
|
schedule_data: ProductionScheduleCreate,
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Generate or manually create a daily/shift schedule"""
|
|
try:
|
|
|
|
schedule = await production_service.create_production_schedule(tenant_id, schedule_data)
|
|
|
|
logger.info("Created production schedule",
|
|
schedule_id=str(schedule.id), tenant_id=str(tenant_id))
|
|
|
|
return ProductionScheduleResponse.model_validate(schedule)
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid schedule data", error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error creating production schedule",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to create production schedule")
|
|
|
|
|
|
@router.put("/tenants/{tenant_id}/production/schedules/{schedule_id}", response_model=ProductionScheduleResponse)
|
|
async def update_production_schedule(
|
|
schedule_update: ProductionScheduleUpdate,
|
|
tenant_id: UUID = Path(...),
|
|
schedule_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Edit schedule before finalizing"""
|
|
try:
|
|
|
|
schedule = await production_service.update_production_schedule(tenant_id, schedule_id, schedule_update)
|
|
|
|
logger.info("Updated production schedule",
|
|
schedule_id=str(schedule_id), tenant_id=str(tenant_id))
|
|
|
|
return ProductionScheduleResponse.model_validate(schedule)
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid schedule update", error=str(e), schedule_id=str(schedule_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error updating production schedule",
|
|
error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to update production schedule")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/schedules/{schedule_id}/finalize", response_model=ProductionScheduleResponse)
|
|
async def finalize_production_schedule(
|
|
tenant_id: UUID = Path(...),
|
|
schedule_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Lock schedule; prevents further changes"""
|
|
try:
|
|
|
|
schedule = await production_service.finalize_production_schedule(tenant_id, schedule_id)
|
|
|
|
logger.info("Finalized production schedule",
|
|
schedule_id=str(schedule_id), tenant_id=str(tenant_id))
|
|
|
|
return ProductionScheduleResponse.model_validate(schedule)
|
|
|
|
except ValueError as e:
|
|
logger.warning("Cannot finalize schedule", error=str(e), schedule_id=str(schedule_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error finalizing production schedule",
|
|
error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to finalize production schedule")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/schedules/{date}/optimize", response_model=dict)
|
|
async def optimize_production_schedule(
|
|
tenant_id: UUID = Path(...),
|
|
target_date: date = Path(..., alias="date"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Trigger AI-based rescheduling suggestion based on demand/capacity"""
|
|
try:
|
|
|
|
optimization_result = await production_service.optimize_schedule(tenant_id, target_date)
|
|
|
|
logger.info("Generated schedule optimization suggestions",
|
|
tenant_id=str(tenant_id), date=target_date.isoformat())
|
|
|
|
return optimization_result
|
|
|
|
except Exception as e:
|
|
logger.error("Error optimizing production schedule",
|
|
error=str(e), tenant_id=str(tenant_id), date=target_date.isoformat())
|
|
raise HTTPException(status_code=500, detail="Failed to optimize production schedule")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/schedules/capacity-usage", response_model=dict)
|
|
async def get_schedule_capacity_usage(
|
|
tenant_id: UUID = Path(...),
|
|
start_date: Optional[date] = Query(None, description="Start date for capacity usage"),
|
|
end_date: Optional[date] = Query(None, description="End date for capacity usage"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""View capacity utilization over time (for reporting)"""
|
|
try:
|
|
|
|
# Default to last 30 days if no dates provided
|
|
if not start_date:
|
|
start_date = (datetime.now() - timedelta(days=30)).date()
|
|
if not end_date:
|
|
end_date = datetime.now().date()
|
|
|
|
capacity_usage = await production_service.get_capacity_usage_report(tenant_id, start_date, end_date)
|
|
|
|
logger.info("Retrieved schedule capacity usage",
|
|
tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat())
|
|
|
|
return capacity_usage
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting schedule capacity usage",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get schedule capacity usage")
|
|
|
|
|
|
# ================================================================
|
|
# CAPACITY MANAGEMENT ENDPOINTS
|
|
# ================================================================
|
|
|
|
@router.get("/tenants/{tenant_id}/production/capacity/status", response_model=dict)
|
|
async def get_capacity_status(
|
|
tenant_id: UUID = Path(...),
|
|
date: Optional[date] = Query(None, description="Date for capacity status"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Get production capacity status for a specific date"""
|
|
try:
|
|
|
|
target_date = date or datetime.now().date()
|
|
|
|
from app.repositories.production_capacity_repository import ProductionCapacityRepository
|
|
capacity_repo = ProductionCapacityRepository(db)
|
|
|
|
capacity_summary = await capacity_repo.get_capacity_utilization_summary(
|
|
str(tenant_id), target_date, target_date
|
|
)
|
|
|
|
logger.info("Retrieved capacity status",
|
|
tenant_id=str(tenant_id), date=target_date.isoformat())
|
|
|
|
return capacity_summary
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting capacity status",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get capacity status")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/capacity", response_model=dict)
|
|
async def list_production_capacity(
|
|
tenant_id: UUID = Path(...),
|
|
resource_type: Optional[str] = Query(None, description="Filter by resource type (equipment/staff)"),
|
|
date: Optional[date] = Query(None, description="Filter by date"),
|
|
availability: Optional[bool] = Query(None, description="Filter by availability"),
|
|
page: int = Query(1, ge=1, description="Page number"),
|
|
page_size: int = Query(50, ge=1, le=100, description="Page size"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Filter by resource_type (equipment/staff), date, availability"""
|
|
try:
|
|
|
|
filters = {
|
|
"resource_type": resource_type,
|
|
"date": date,
|
|
"availability": availability
|
|
}
|
|
|
|
capacity_list = await production_service.get_capacity_list(tenant_id, filters, page, page_size)
|
|
|
|
logger.info("Retrieved production capacity list",
|
|
tenant_id=str(tenant_id), filters=filters)
|
|
|
|
return capacity_list
|
|
|
|
except Exception as e:
|
|
logger.error("Error listing production capacity",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to list production capacity")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/capacity/{resource_id}/availability", response_model=dict)
|
|
async def check_resource_availability(
|
|
tenant_id: UUID = Path(...),
|
|
resource_id: str = Path(...),
|
|
start_time: datetime = Query(..., description="Start time for availability check"),
|
|
end_time: datetime = Query(..., description="End time for availability check"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Check if oven/station is free during a time window"""
|
|
try:
|
|
|
|
availability = await production_service.check_resource_availability(
|
|
tenant_id, resource_id, start_time, end_time
|
|
)
|
|
|
|
logger.info("Checked resource availability",
|
|
tenant_id=str(tenant_id), resource_id=resource_id)
|
|
|
|
return availability
|
|
|
|
except Exception as e:
|
|
logger.error("Error checking resource availability",
|
|
error=str(e), tenant_id=str(tenant_id), resource_id=resource_id)
|
|
raise HTTPException(status_code=500, detail="Failed to check resource availability")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/capacity/reserve", response_model=dict)
|
|
async def reserve_capacity(
|
|
reservation_data: dict,
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Reserve equipment/staff for a future batch"""
|
|
try:
|
|
|
|
reservation = await production_service.reserve_capacity(tenant_id, reservation_data)
|
|
|
|
logger.info("Reserved production capacity",
|
|
tenant_id=str(tenant_id))
|
|
|
|
return reservation
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid reservation data", error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error reserving capacity",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to reserve capacity")
|
|
|
|
|
|
@router.put("/tenants/{tenant_id}/production/capacity/{capacity_id}", response_model=dict)
|
|
async def update_capacity(
|
|
capacity_update: dict,
|
|
tenant_id: UUID = Path(...),
|
|
capacity_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Update maintenance status or efficiency rating"""
|
|
try:
|
|
|
|
updated_capacity = await production_service.update_capacity(tenant_id, capacity_id, capacity_update)
|
|
|
|
logger.info("Updated production capacity",
|
|
tenant_id=str(tenant_id), capacity_id=str(capacity_id))
|
|
|
|
return updated_capacity
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid capacity update", error=str(e), capacity_id=str(capacity_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error updating capacity",
|
|
error=str(e), tenant_id=str(tenant_id), capacity_id=str(capacity_id))
|
|
raise HTTPException(status_code=500, detail="Failed to update capacity")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/capacity/bottlenecks", response_model=dict)
|
|
async def get_capacity_bottlenecks(
|
|
tenant_id: UUID = Path(...),
|
|
days_ahead: int = Query(3, ge=1, le=30, description="Number of days to predict ahead"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""AI-powered endpoint: returns predicted bottlenecks for next 3 days"""
|
|
try:
|
|
|
|
bottlenecks = await production_service.predict_capacity_bottlenecks(tenant_id, days_ahead)
|
|
|
|
logger.info("Retrieved capacity bottleneck predictions",
|
|
tenant_id=str(tenant_id), days_ahead=days_ahead)
|
|
|
|
return bottlenecks
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting capacity bottlenecks",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get capacity bottlenecks")
|
|
|
|
|
|
# ================================================================
|
|
# QUALITY CHECK ENDPOINTS
|
|
# ================================================================
|
|
|
|
@router.get("/tenants/{tenant_id}/production/quality-checks", response_model=dict)
|
|
async def list_quality_checks(
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: Optional[UUID] = Query(None, description="Filter by batch"),
|
|
product_id: Optional[UUID] = Query(None, description="Filter by product"),
|
|
start_date: Optional[date] = Query(None, description="Filter from date"),
|
|
end_date: Optional[date] = Query(None, description="Filter to date"),
|
|
pass_fail: Optional[bool] = Query(None, description="Filter by pass/fail"),
|
|
page: int = Query(1, ge=1, description="Page number"),
|
|
page_size: int = Query(50, ge=1, le=100, description="Page size"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""List checks filtered by batch, product, date, pass/fail"""
|
|
try:
|
|
|
|
filters = {
|
|
"batch_id": str(batch_id) if batch_id else None,
|
|
"product_id": str(product_id) if product_id else None,
|
|
"start_date": start_date,
|
|
"end_date": end_date,
|
|
"pass_fail": pass_fail
|
|
}
|
|
|
|
quality_checks = await production_service.get_quality_checks_list(tenant_id, filters, page, page_size)
|
|
|
|
logger.info("Retrieved quality checks list",
|
|
tenant_id=str(tenant_id), filters=filters)
|
|
|
|
return quality_checks
|
|
|
|
except Exception as e:
|
|
logger.error("Error listing quality checks",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to list quality checks")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/batches/{batch_id}/quality-checks", response_model=dict)
|
|
async def get_batch_quality_checks(
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Get all quality checks for a specific batch"""
|
|
try:
|
|
|
|
quality_checks = await production_service.get_batch_quality_checks(tenant_id, batch_id)
|
|
|
|
logger.info("Retrieved quality checks for batch",
|
|
tenant_id=str(tenant_id), batch_id=str(batch_id))
|
|
|
|
return quality_checks
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting batch quality checks",
|
|
error=str(e), tenant_id=str(tenant_id), batch_id=str(batch_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get batch quality checks")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/quality-checks", response_model=dict)
|
|
async def create_quality_check(
|
|
quality_check_data: dict,
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Submit a new quality inspection result"""
|
|
try:
|
|
|
|
quality_check = await production_service.create_quality_check(tenant_id, quality_check_data)
|
|
|
|
logger.info("Created quality check",
|
|
tenant_id=str(tenant_id))
|
|
|
|
return quality_check
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid quality check data", error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error creating quality check",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to create quality check")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/quality-checks/trends", response_model=dict)
|
|
async def get_quality_trends(
|
|
tenant_id: UUID = Path(...),
|
|
start_date: Optional[date] = Query(None, description="Start date for trends"),
|
|
end_date: Optional[date] = Query(None, description="End date for trends"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Returns defect trends, average scores by product/equipment"""
|
|
try:
|
|
|
|
# Default to last 30 days if no dates provided
|
|
if not start_date:
|
|
start_date = (datetime.now() - timedelta(days=30)).date()
|
|
if not end_date:
|
|
end_date = datetime.now().date()
|
|
|
|
trends = await production_service.get_quality_trends(tenant_id, start_date, end_date)
|
|
|
|
logger.info("Retrieved quality trends",
|
|
tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat())
|
|
|
|
return trends
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting quality trends",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get quality trends")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/quality-checks/alerts", response_model=dict)
|
|
async def get_quality_alerts(
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Active alerts where corrective action is needed"""
|
|
try:
|
|
|
|
alerts = await production_service.get_quality_alerts(tenant_id)
|
|
|
|
logger.info("Retrieved quality alerts",
|
|
tenant_id=str(tenant_id))
|
|
|
|
return alerts
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting quality alerts",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get quality alerts")
|
|
|
|
|
|
@router.put("/tenants/{tenant_id}/production/quality-checks/{check_id}", response_model=dict)
|
|
async def update_quality_check(
|
|
check_update: dict,
|
|
tenant_id: UUID = Path(...),
|
|
check_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Add photos, notes, or mark corrective actions as completed"""
|
|
try:
|
|
|
|
updated_check = await production_service.update_quality_check(tenant_id, check_id, check_update)
|
|
|
|
logger.info("Updated quality check",
|
|
tenant_id=str(tenant_id), check_id=str(check_id))
|
|
|
|
return updated_check
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid quality check update", error=str(e), check_id=str(check_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error updating quality check",
|
|
error=str(e), tenant_id=str(tenant_id), check_id=str(check_id))
|
|
raise HTTPException(status_code=500, detail="Failed to update quality check")
|
|
|
|
|
|
# ================================================================
|
|
# ANALYTICS / CROSS-CUTTING ENDPOINTS
|
|
# ================================================================
|
|
|
|
@router.get("/tenants/{tenant_id}/production/analytics/performance", response_model=dict)
|
|
async def get_performance_analytics(
|
|
tenant_id: UUID = Path(...),
|
|
start_date: Optional[date] = Query(None, description="Start date for analytics"),
|
|
end_date: Optional[date] = Query(None, description="End date for analytics"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Daily performance: completion rate, waste %, labor cost per unit"""
|
|
try:
|
|
|
|
# Default to last 30 days if no dates provided
|
|
if not start_date:
|
|
start_date = (datetime.now() - timedelta(days=30)).date()
|
|
if not end_date:
|
|
end_date = datetime.now().date()
|
|
|
|
performance = await production_service.get_performance_analytics(tenant_id, start_date, end_date)
|
|
|
|
logger.info("Retrieved performance analytics",
|
|
tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat())
|
|
|
|
return performance
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting performance analytics",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get performance analytics")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/analytics/yield-trends", response_model=dict)
|
|
async def get_yield_trends_analytics(
|
|
tenant_id: UUID = Path(...),
|
|
period: str = Query("week", regex="^(week|month)$", description="Time period for trends"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Yield trendline by product over past week/month"""
|
|
try:
|
|
|
|
yield_trends = await production_service.get_yield_trends_analytics(tenant_id, period)
|
|
|
|
logger.info("Retrieved yield trends analytics",
|
|
tenant_id=str(tenant_id), period=period)
|
|
|
|
return yield_trends
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting yield trends analytics",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get yield trends analytics")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/analytics/top-defects", response_model=dict)
|
|
async def get_top_defects_analytics(
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Top 5 defect types across batches"""
|
|
try:
|
|
|
|
top_defects = await production_service.get_top_defects_analytics(tenant_id)
|
|
|
|
logger.info("Retrieved top defects analytics",
|
|
tenant_id=str(tenant_id))
|
|
|
|
return top_defects
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting top defects analytics",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get top defects analytics")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/analytics/equipment-efficiency", response_model=dict)
|
|
async def get_equipment_efficiency_analytics(
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Rank ovens/mixers by uptime, yield, downtime"""
|
|
try:
|
|
|
|
equipment_efficiency = await production_service.get_equipment_efficiency_analytics(tenant_id)
|
|
|
|
logger.info("Retrieved equipment efficiency analytics",
|
|
tenant_id=str(tenant_id))
|
|
|
|
return equipment_efficiency
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting equipment efficiency analytics",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get equipment efficiency analytics")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/analytics/generate-report", response_model=dict)
|
|
async def generate_analytics_report(
|
|
report_config: dict,
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Generate PDF report (daily summary, compliance audit)"""
|
|
try:
|
|
|
|
report = await production_service.generate_analytics_report(tenant_id, report_config)
|
|
|
|
logger.info("Generated analytics report",
|
|
tenant_id=str(tenant_id))
|
|
|
|
return report
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid report configuration", error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error generating analytics report",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to generate analytics report")
|
|
|
|
|
|
# ================================================================
|
|
# METRICS AND ANALYTICS ENDPOINTS
|
|
# ================================================================
|
|
|
|
@router.get("/tenants/{tenant_id}/production/metrics/yield", response_model=dict)
|
|
async def get_yield_metrics(
|
|
tenant_id: UUID = Path(...),
|
|
start_date: date = Query(..., description="Start date for metrics"),
|
|
end_date: date = Query(..., description="End date for metrics"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Get production yield metrics for analysis"""
|
|
try:
|
|
|
|
from app.repositories.production_batch_repository import ProductionBatchRepository
|
|
batch_repo = ProductionBatchRepository(db)
|
|
|
|
metrics = await batch_repo.get_production_metrics(str(tenant_id), start_date, end_date)
|
|
|
|
logger.info("Retrieved yield metrics",
|
|
tenant_id=str(tenant_id),
|
|
start_date=start_date.isoformat(),
|
|
end_date=end_date.isoformat())
|
|
|
|
return metrics
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting yield metrics",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get yield metrics")
|
|
|
|
|
|
# ================================================================
|
|
# QUALITY TEMPLATES ENDPOINTS
|
|
# ================================================================
|
|
|
|
from app.repositories.quality_template_repository import QualityTemplateRepository
|
|
from app.schemas.quality_templates import (
|
|
QualityCheckTemplateCreate,
|
|
QualityCheckTemplateUpdate,
|
|
QualityCheckTemplateResponse,
|
|
QualityCheckTemplateList
|
|
)
|
|
|
|
@router.get("/tenants/{tenant_id}/production/quality-templates", response_model=QualityCheckTemplateList)
|
|
async def get_quality_templates(
|
|
tenant_id: UUID = Path(...),
|
|
stage: Optional[str] = Query(None, description="Filter by process stage"),
|
|
check_type: Optional[str] = Query(None, description="Filter by check type"),
|
|
is_active: Optional[bool] = Query(True, description="Filter by active status"),
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=1000),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Get quality check templates for tenant"""
|
|
try:
|
|
repo = QualityTemplateRepository(db)
|
|
|
|
# Convert stage string to ProcessStage enum if provided
|
|
stage_enum = None
|
|
if stage:
|
|
try:
|
|
stage_enum = ProcessStage(stage)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail=f"Invalid stage: {stage}")
|
|
|
|
templates, total = await repo.get_templates_by_tenant(
|
|
tenant_id=str(tenant_id),
|
|
stage=stage_enum,
|
|
check_type=check_type,
|
|
is_active=is_active,
|
|
skip=skip,
|
|
limit=limit
|
|
)
|
|
|
|
return QualityCheckTemplateList(
|
|
templates=[QualityCheckTemplateResponse.from_orm(t) for t in templates],
|
|
total=total,
|
|
skip=skip,
|
|
limit=limit
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error getting quality templates",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get quality templates")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/quality-templates", response_model=QualityCheckTemplateResponse)
|
|
async def create_quality_template(
|
|
template_data: QualityCheckTemplateCreate,
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Create a new quality check template"""
|
|
try:
|
|
repo = QualityTemplateRepository(db)
|
|
|
|
# Add tenant_id to the template data
|
|
create_data = template_data.dict()
|
|
create_data['tenant_id'] = str(tenant_id)
|
|
|
|
template = await repo.create(create_data)
|
|
return QualityCheckTemplateResponse.from_orm(template)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error creating quality template",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to create quality template")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/quality-templates/{template_id}", response_model=QualityCheckTemplateResponse)
|
|
async def get_quality_template(
|
|
tenant_id: UUID = Path(...),
|
|
template_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Get a specific quality check template"""
|
|
try:
|
|
repo = QualityTemplateRepository(db)
|
|
template = await repo.get_by_tenant_and_id(str(tenant_id), template_id)
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="Quality template not found")
|
|
return QualityCheckTemplateResponse.from_orm(template)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error getting quality template",
|
|
error=str(e), tenant_id=str(tenant_id), template_id=str(template_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get quality template")
|
|
|
|
|
|
@router.put("/tenants/{tenant_id}/production/quality-templates/{template_id}", response_model=QualityCheckTemplateResponse)
|
|
async def update_quality_template(
|
|
template_data: QualityCheckTemplateUpdate,
|
|
tenant_id: UUID = Path(...),
|
|
template_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Update a quality check template"""
|
|
try:
|
|
repo = QualityTemplateRepository(db)
|
|
# First check if template exists and belongs to tenant
|
|
existing = await repo.get_by_tenant_and_id(str(tenant_id), template_id)
|
|
if not existing:
|
|
raise HTTPException(status_code=404, detail="Quality template not found")
|
|
|
|
template = await repo.update(template_id, template_data.dict(exclude_unset=True))
|
|
return QualityCheckTemplateResponse.from_orm(template)
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error updating quality template",
|
|
error=str(e), tenant_id=str(tenant_id), template_id=str(template_id))
|
|
raise HTTPException(status_code=500, detail="Failed to update quality template")
|
|
|
|
|
|
@router.delete("/tenants/{tenant_id}/production/quality-templates/{template_id}")
|
|
async def delete_quality_template(
|
|
tenant_id: UUID = Path(...),
|
|
template_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Delete a quality check template"""
|
|
try:
|
|
repo = QualityTemplateRepository(db)
|
|
# First check if template exists and belongs to tenant
|
|
existing = await repo.get_by_tenant_and_id(str(tenant_id), template_id)
|
|
if not existing:
|
|
raise HTTPException(status_code=404, detail="Quality template not found")
|
|
|
|
await repo.delete(template_id)
|
|
return {"message": "Quality template deleted successfully"}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error deleting quality template",
|
|
error=str(e), tenant_id=str(tenant_id), template_id=str(template_id))
|
|
raise HTTPException(status_code=500, detail="Failed to delete quality template")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/quality-templates/{template_id}/duplicate", response_model=QualityCheckTemplateResponse)
|
|
async def duplicate_quality_template(
|
|
tenant_id: UUID = Path(...),
|
|
template_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db=Depends(get_db)
|
|
):
|
|
"""Duplicate an existing quality check template"""
|
|
try:
|
|
repo = QualityTemplateRepository(db)
|
|
# Get original template
|
|
original = await repo.get_by_tenant_and_id(str(tenant_id), template_id)
|
|
if not original:
|
|
raise HTTPException(status_code=404, detail="Quality template not found")
|
|
|
|
# Create duplicate data
|
|
duplicate_data = {
|
|
"tenant_id": original.tenant_id,
|
|
"name": f"{original.name} (Copy)",
|
|
"template_code": None, # Will be auto-generated
|
|
"check_type": original.check_type,
|
|
"category": original.category,
|
|
"description": original.description,
|
|
"instructions": original.instructions,
|
|
"criteria": original.criteria,
|
|
"is_required": original.is_required,
|
|
"is_critical": original.is_critical,
|
|
"weight": original.weight,
|
|
"min_value": original.min_value,
|
|
"max_value": original.max_value,
|
|
"unit": original.unit,
|
|
"tolerance_percentage": original.tolerance_percentage,
|
|
"applicable_stages": original.applicable_stages,
|
|
"created_by": original.created_by
|
|
}
|
|
|
|
template = await repo.create(duplicate_data)
|
|
return QualityCheckTemplateResponse.from_orm(template)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error duplicating quality template",
|
|
error=str(e), tenant_id=str(tenant_id), template_id=str(template_id))
|
|
raise HTTPException(status_code=500, detail="Failed to duplicate quality template")
|
|
|
|
|
|
# ================================================================
|
|
# TRANSFORMATION ENDPOINTS
|
|
# ================================================================
|
|
|
|
@router.post("/tenants/{tenant_id}/production/batches/{batch_id}/complete-with-transformation", response_model=dict)
|
|
async def complete_batch_with_transformation(
|
|
transformation_data: Optional[dict] = None,
|
|
completion_data: Optional[dict] = None,
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Complete batch and apply transformation (e.g. par-baked to fully baked)"""
|
|
try:
|
|
result = await production_service.complete_production_batch_with_transformation(
|
|
tenant_id, batch_id, completion_data, transformation_data
|
|
)
|
|
|
|
logger.info("Completed batch with transformation",
|
|
batch_id=str(batch_id),
|
|
has_transformation=bool(transformation_data),
|
|
tenant_id=str(tenant_id))
|
|
|
|
return result
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid batch completion with transformation", error=str(e), batch_id=str(batch_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error completing batch with transformation",
|
|
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to complete batch with transformation")
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/production/transformations/par-baked-to-fresh", response_model=dict)
|
|
async def transform_par_baked_products(
|
|
source_ingredient_id: UUID = Query(..., description="Par-baked ingredient ID"),
|
|
target_ingredient_id: UUID = Query(..., description="Fresh baked ingredient ID"),
|
|
quantity: float = Query(..., gt=0, description="Quantity to transform"),
|
|
batch_reference: Optional[str] = Query(None, description="Production batch reference"),
|
|
expiration_hours: int = Query(24, ge=1, le=72, description="Hours until expiration after transformation"),
|
|
notes: Optional[str] = Query(None, description="Transformation notes"),
|
|
tenant_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Transform par-baked products to fresh baked products"""
|
|
try:
|
|
result = await production_service.transform_par_baked_products(
|
|
tenant_id=tenant_id,
|
|
source_ingredient_id=source_ingredient_id,
|
|
target_ingredient_id=target_ingredient_id,
|
|
quantity=quantity,
|
|
batch_reference=batch_reference,
|
|
expiration_hours=expiration_hours,
|
|
notes=notes
|
|
)
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=400, detail="Failed to create transformation")
|
|
|
|
logger.info("Transformed par-baked products to fresh",
|
|
transformation_id=result.get('transformation_id'),
|
|
quantity=quantity, tenant_id=str(tenant_id))
|
|
|
|
return result
|
|
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
logger.warning("Invalid transformation data", error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Error transforming par-baked products",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to transform par-baked products")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/transformations", response_model=dict)
|
|
async def get_production_transformations(
|
|
tenant_id: UUID = Path(...),
|
|
days_back: int = Query(30, ge=1, le=365, description="Days back to retrieve transformations"),
|
|
limit: int = Query(100, ge=1, le=500, description="Maximum number of transformations to retrieve"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Get transformations related to production processes"""
|
|
try:
|
|
transformations = await production_service.get_production_transformations(
|
|
tenant_id, days_back, limit
|
|
)
|
|
|
|
result = {
|
|
"transformations": transformations,
|
|
"total_count": len(transformations),
|
|
"period_days": days_back,
|
|
"retrieved_at": datetime.now().isoformat()
|
|
}
|
|
|
|
logger.info("Retrieved production transformations",
|
|
count=len(transformations), tenant_id=str(tenant_id))
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting production transformations",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get production transformations")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/analytics/transformation-efficiency", response_model=dict)
|
|
async def get_transformation_efficiency_analytics(
|
|
tenant_id: UUID = Path(...),
|
|
days_back: int = Query(30, ge=1, le=365, description="Days back for efficiency analysis"),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Get transformation efficiency metrics for analytics"""
|
|
try:
|
|
metrics = await production_service.get_transformation_efficiency_metrics(
|
|
tenant_id, days_back
|
|
)
|
|
|
|
logger.info("Retrieved transformation efficiency analytics",
|
|
total_transformations=metrics.get('total_transformations', 0),
|
|
tenant_id=str(tenant_id))
|
|
|
|
return metrics
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting transformation efficiency analytics",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get transformation efficiency analytics")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/batches/{batch_id}/transformations", response_model=dict)
|
|
async def get_batch_transformations(
|
|
tenant_id: UUID = Path(...),
|
|
batch_id: UUID = Path(...),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Get batch details with associated transformations"""
|
|
try:
|
|
result = await production_service.get_batch_with_transformations(tenant_id, batch_id)
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Batch not found")
|
|
|
|
logger.info("Retrieved batch with transformations",
|
|
batch_id=str(batch_id),
|
|
transformation_count=result.get('transformation_count', 0),
|
|
tenant_id=str(tenant_id))
|
|
|
|
return result
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error getting batch transformations",
|
|
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get batch transformations") |