aaredb/backend/app/routers/logistics.py
2024-12-02 13:36:48 +01:00

281 lines
12 KiB
Python

from fastapi import APIRouter, HTTPException, Depends
from pydantic import ValidationError
from sqlalchemy.orm import Session, joinedload
from typing import List, Optional
from ..models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel
from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema
from ..database import get_db
import logging
from datetime import datetime, timedelta
router = APIRouter()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval_hours: int = 1) -> int:
refill_interval = timedelta(hours=refill_interval_hours)
now = datetime.now()
if not last_refill:
return -1 # Sentinel value indicating no refill event recorded
time_until_next_refill = last_refill + refill_interval - now
return max(0, int(time_until_next_refill.total_seconds()))
@router.post("/dewars/return", response_model=DewarSchema)
async def return_to_storage(data: LogisticsEventCreate, db: Session = Depends(get_db)):
logger.info(f"Returning dewar to storage: {data.dewar_qr_code} at location {data.location_qr_code}")
try:
# Log the incoming payload
logger.info("Received payload: %s", data.json())
dewar = db.query(DewarModel).filter(DewarModel.unique_id == data.dewar_qr_code).first()
if not dewar:
logger.error(f"Dewar not found for unique ID: {data.dewar_qr_code}")
raise HTTPException(status_code=404, detail="Dewar not found")
original_slot = db.query(SlotModel).filter(SlotModel.dewar_unique_id == data.dewar_qr_code).first()
if original_slot and original_slot.qr_code != data.location_qr_code:
logger.error(f"Dewar {data.dewar_qr_code} is associated with slot {original_slot.qr_code}")
raise HTTPException(status_code=400, detail=f"Dewar {data.dewar_qr_code} is associated with a different slot {original_slot.qr_code}.")
slot = db.query(SlotModel).filter(SlotModel.qr_code == data.location_qr_code).first()
if not slot:
logger.error(f"Slot not found for QR code: {data.location_qr_code}")
raise HTTPException(status_code=404, detail="Slot not found")
if slot.occupied and slot.dewar_unique_id != data.dewar_qr_code:
logger.error(f"Slot {data.location_qr_code} is already occupied by another dewar")
raise HTTPException(status_code=400, detail="Selected slot is already occupied by another dewar")
# Update slot with dewar information
slot.dewar_unique_id = dewar.unique_id
slot.occupied = True
dewar.last_retrieved_timestamp = None
# Log the event
log_event(db, dewar.id, slot.id, "returned")
db.commit()
logger.info(f"Dewar {data.dewar_qr_code} successfully returned to storage slot {slot.qr_code}.")
db.refresh(dewar)
return dewar
except ValidationError as e:
logger.error(f"Validation error: {e.json()}")
raise HTTPException(status_code=400, detail="Invalid payload")
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/dewar/scan", response_model=dict)
async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)):
logger.info(f"Received event data: {event_data}")
dewar_qr_code = event_data.dewar_qr_code
location_qr_code = event_data.location_qr_code
transaction_type = event_data.transaction_type
# Validate Dewar QR Code
if not dewar_qr_code or not dewar_qr_code.strip():
logger.error("Dewar QR Code is null or empty")
raise HTTPException(status_code=422, detail="Dewar QR Code cannot be null or empty")
# Retrieve the Dewar
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
if not dewar:
logger.error("Dewar not found")
raise HTTPException(status_code=404, detail="Dewar not found")
# Check for Outgoing QR Codes and set transaction type
if location_qr_code in ["Outgoing X10-SA", "Outgoing X06-SA"]:
transaction_type = 'outgoing'
# Retrieve the Slot associated with the Dewar (for outgoing)
slot = None
if transaction_type == 'outgoing':
slot = db.query(SlotModel).filter(SlotModel.dewar_unique_id == dewar.unique_id).first()
if not slot:
logger.error(f"No slot associated with dewar for outgoing: {dewar_qr_code}")
raise HTTPException(status_code=404, detail="No slot associated with dewar for outgoing")
# Incoming Logic
if transaction_type == 'incoming':
slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first()
if not slot or slot.occupied:
logger.error(f"Slot not found or already occupied: {location_qr_code}")
raise HTTPException(status_code=400, detail="Slot not found or already occupied")
slot.dewar_unique_id = dewar.unique_id
slot.occupied = True
elif transaction_type == 'outgoing':
if not slot.occupied or slot.dewar_unique_id != dewar.unique_id:
logger.error(f"Slot not valid for outgoing: {location_qr_code}")
raise HTTPException(status_code=400, detail="Dewar not associated with the slot for outgoing")
slot.dewar_unique_id = None
slot.occupied = False
elif transaction_type == 'beamline':
slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first()
if not slot:
logger.error(f"Beamline location not found: {location_qr_code}")
raise HTTPException(status_code=400, detail="Beamline location not found")
dewar.beamline_location = location_qr_code
logger.info(f"Dewar {dewar_qr_code} assigned to beamline {location_qr_code}")
# Log the event
log_event(db, dewar.id, slot.id if slot else None, transaction_type)
db.commit()
logger.info(
f"Transaction completed: {transaction_type} for dewar {dewar_qr_code} in slot {slot.qr_code if slot else 'N/A'}")
return {"message": "Status updated successfully"}
@router.get("/slots", response_model=List[SlotSchema])
async def get_all_slots(db: Session = Depends(get_db)):
slots = db.query(SlotModel).options(joinedload(SlotModel.dewar)).all()
slots_with_refill_time = []
for slot in slots:
time_until_refill = None
retrievedTimestamp = None
beamlineLocation = None
at_beamline = False
retrieved = False
if slot.dewar_unique_id:
# Calculate time until refill
last_refill_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
.filter(
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "refill"
) \
.order_by(LogisticsEventModel.timestamp.desc()) \
.first()
if last_refill_event:
last_refill = last_refill_event.timestamp
time_until_refill = calculate_time_until_refill(last_refill)
else:
time_until_refill = -1
# Get last retrieved timestamp
last_retrieved_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
.filter(
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "retrieved"
) \
.order_by(LogisticsEventModel.timestamp.desc()) \
.first()
if last_retrieved_event:
retrievedTimestamp = last_retrieved_event.timestamp.isoformat()
retrieved = True
# Determine the last event excluding refills
last_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
.filter(
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type != "refill"
) \
.order_by(LogisticsEventModel.timestamp.desc()) \
.first()
if last_event:
associated_slot = db.query(SlotModel).filter(SlotModel.id == last_event.slot_id).first()
beamlineLocation = associated_slot.label if associated_slot else None
at_beamline = last_event.event_type == "beamline"
# Corrected the contact_person assignment
contact_person = None
if slot.dewar and slot.dewar.contact_person:
first_name = slot.dewar.contact_person.firstname
last_name = slot.dewar.contact_person.lastname
contact_person = f"{first_name} {last_name}"
slot_data = SlotSchema(
id=slot.id,
qr_code=slot.qr_code,
label=slot.label,
qr_base=slot.qr_base,
occupied=slot.occupied,
needs_refill=slot.needs_refill,
dewar_unique_id=slot.dewar_unique_id,
dewar_name=slot.dewar.dewar_name if slot.dewar else None,
time_until_refill=time_until_refill,
at_beamline=at_beamline,
retrieved=retrieved,
retrievedTimestamp=retrievedTimestamp,
beamlineLocation=beamlineLocation,
shipment_name=slot.dewar.shipment.shipment_name if slot.dewar and slot.dewar.shipment else None,
contact_person=contact_person,
local_contact='local contact placeholder'
)
logger.info(f"Dewar retrieved: {retrieved}")
logger.info(f"Dewar at: {beamlineLocation}")
slots_with_refill_time.append(slot_data)
return slots_with_refill_time
@router.post("/dewar/refill", response_model=dict)
async def refill_dewar(qr_code: str, db: Session = Depends(get_db)):
logger.info(f"Refilling dewar with QR code: {qr_code}")
dewar = db.query(DewarModel).filter(DewarModel.unique_id == qr_code.strip()).first()
if not dewar:
logger.error("Dewar not found")
raise HTTPException(status_code=404, detail="Dewar not found")
now = datetime.now()
new_event = LogisticsEventModel(
dewar_id=dewar.id,
slot_id=None,
event_type="refill",
timestamp=now,
)
db.add(new_event)
db.commit()
time_until_refill_seconds = calculate_time_until_refill(now)
logger.info(f"Dewar refilled successfully with time_until_refill: {time_until_refill_seconds}")
return {"message": "Dewar refilled successfully", "time_until_refill": time_until_refill_seconds}
@router.get("/dewars", response_model=List[DewarSchema])
async def get_all_dewars(db: Session = Depends(get_db)):
dewars = db.query(DewarModel).all()
return dewars
@router.get("/dewar/{unique_id}", response_model=DewarSchema)
async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)):
logger.info(f"Received request for dewar with unique_id: {unique_id}")
dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id.strip()).first()
if not dewar:
logger.warning(f"Dewar with unique_id '{unique_id}' not found.")
raise HTTPException(status_code=404, detail="Dewar not found")
logger.info(f"Returning dewar: {dewar}")
return dewar
def log_event(db: Session, dewar_id: int, slot_id: Optional[int], event_type: str):
new_event = LogisticsEventModel(
dewar_id=dewar_id,
slot_id=slot_id,
event_type=event_type,
timestamp=datetime.now()
)
db.add(new_event)
db.commit()
logger.info(f"Logged event: {event_type} for dewar: {dewar_id} in slot: {slot_id if slot_id else 'N/A'}")