2025-10-06 15:27:01 +02:00
|
|
|
# services/production/app/api/production_schedules.py
|
|
|
|
|
"""
|
|
|
|
|
Production Schedules API - ATOMIC CRUD operations on ProductionSchedule model
|
|
|
|
|
"""
|
|
|
|
|
|
2025-12-13 23:57:54 +01:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
|
2025-10-06 15:27:01 +02:00
|
|
|
from typing import Optional
|
|
|
|
|
from datetime import date, datetime, timedelta
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
import structlog
|
|
|
|
|
|
|
|
|
|
from shared.auth.decorators import get_current_user_dep
|
2025-10-15 16:12:49 +02:00
|
|
|
from shared.auth.access_control import require_user_role
|
2025-10-06 15:27:01 +02:00
|
|
|
from shared.routing import RouteBuilder
|
2025-10-15 16:12:49 +02:00
|
|
|
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
2025-10-06 15:27:01 +02:00
|
|
|
from app.core.database import get_db
|
|
|
|
|
from app.services.production_service import ProductionService
|
2025-10-29 06:58:05 +01:00
|
|
|
from app.models import AuditLog
|
2025-10-06 15:27:01 +02:00
|
|
|
from app.schemas.production import (
|
|
|
|
|
ProductionScheduleCreate,
|
|
|
|
|
ProductionScheduleUpdate,
|
|
|
|
|
ProductionScheduleResponse
|
|
|
|
|
)
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
route_builder = RouteBuilder('production')
|
|
|
|
|
router = APIRouter(tags=["production-schedules"])
|
|
|
|
|
|
2025-10-29 06:58:05 +01:00
|
|
|
# Initialize audit logger with the production service's AuditLog model
|
|
|
|
|
audit_logger = create_audit_logger("production-service", AuditLog)
|
2025-10-15 16:12:49 +02:00
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
|
2025-12-13 23:57:54 +01:00
|
|
|
def get_production_service(request: Request) -> ProductionService:
|
2025-10-06 15:27:01 +02:00
|
|
|
"""Dependency injection for production service"""
|
|
|
|
|
from app.core.database import database_manager
|
2025-12-13 23:57:54 +01:00
|
|
|
notification_service = getattr(request.app.state, 'notification_service', None)
|
|
|
|
|
return ProductionService(database_manager, settings, notification_service)
|
2025-10-06 15:27:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
route_builder.build_base_route("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(
|
|
|
|
|
route_builder.build_resource_detail_route("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(
|
|
|
|
|
route_builder.build_base_route("schedules"),
|
|
|
|
|
response_model=ProductionScheduleResponse
|
|
|
|
|
)
|
2025-10-15 16:12:49 +02:00
|
|
|
@require_user_role(['admin', 'owner'])
|
2025-10-06 15:27:01 +02:00
|
|
|
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)
|
|
|
|
|
):
|
2025-10-15 16:12:49 +02:00
|
|
|
"""Generate or manually create a daily/shift schedule (Admin+ only)"""
|
2025-10-06 15:27:01 +02:00
|
|
|
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(
|
|
|
|
|
route_builder.build_resource_detail_route("schedules", "schedule_id"),
|
|
|
|
|
response_model=ProductionScheduleResponse
|
|
|
|
|
)
|
2025-10-15 16:12:49 +02:00
|
|
|
@require_user_role(['admin', 'owner'])
|
2025-10-06 15:27:01 +02:00
|
|
|
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)
|
|
|
|
|
):
|
2025-10-15 16:12:49 +02:00
|
|
|
"""Edit schedule before finalizing (Admin+ only)"""
|
2025-10-06 15:27:01 +02:00
|
|
|
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.delete(
|
|
|
|
|
route_builder.build_resource_detail_route("schedules", "schedule_id")
|
|
|
|
|
)
|
|
|
|
|
async def delete_production_schedule(
|
|
|
|
|
tenant_id: UUID = Path(...),
|
|
|
|
|
schedule_id: UUID = Path(...),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Delete a production schedule (if not finalized)"""
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
if schedule.is_finalized:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Cannot delete finalized schedule")
|
|
|
|
|
|
|
|
|
|
await schedule_repo.delete(schedule_id)
|
|
|
|
|
|
|
|
|
|
logger.info("Deleted production schedule",
|
|
|
|
|
schedule_id=str(schedule_id), tenant_id=str(tenant_id))
|
|
|
|
|
|
|
|
|
|
return {"message": "Production schedule deleted successfully"}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error deleting production schedule",
|
|
|
|
|
error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to delete production schedule")
|