388 lines
16 KiB
Python
388 lines
16 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,
|
|
DailyProductionRequirements, ProductionDashboardSummary, ProductionMetrics,
|
|
)
|
|
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:
|
|
# Extract tenant from user context for security
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
summary = await production_service.get_dashboard_summary(tenant_id)
|
|
|
|
logger.info("Retrieved production dashboard summary",
|
|
tenant_id=str(tenant_id), user_id=current_user.get("user_id"))
|
|
|
|
return summary
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting production dashboard summary",
|
|
error=str(e), tenant_id=str(tenant_id))
|
|
raise HTTPException(status_code=500, detail="Failed to get dashboard summary")
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/production/daily-requirements", response_model=DailyProductionRequirements)
|
|
async def get_daily_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 daily production requirements"""
|
|
try:
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
target_date = date or datetime.now().date()
|
|
requirements = await production_service.calculate_daily_requirements(tenant_id, target_date)
|
|
|
|
logger.info("Retrieved daily production requirements",
|
|
tenant_id=str(tenant_id), date=target_date.isoformat())
|
|
|
|
return requirements
|
|
|
|
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:
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
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.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:
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
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:
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
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:
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
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:
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
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")
|
|
|
|
|
|
# ================================================================
|
|
# PRODUCTION SCHEDULE ENDPOINTS
|
|
# ================================================================
|
|
|
|
@router.get("/tenants/{tenant_id}/production/schedule", 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:
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
# 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")
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
# 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:
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
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")
|
|
|
|
|
|
# ================================================================
|
|
# 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:
|
|
current_tenant = current_user.get("tenant_id")
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
|
|
|
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") |