added a router for logistics, now creating label
This commit is contained in:
parent
6083c72a1d
commit
0eb0bc3486
@ -2,7 +2,6 @@ import logging
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from app.models import Shipment
|
||||
|
||||
|
||||
def get_shipments(db: Session):
|
||||
logging.info("Fetching all shipments from the database.")
|
||||
shipments = db.query(Shipment).options(
|
||||
@ -18,7 +17,6 @@ def get_shipments(db: Session):
|
||||
logging.debug(f"Shipment ID: {shipment.id}, Shipment Name: {shipment.shipment_name}")
|
||||
return shipments
|
||||
|
||||
|
||||
def get_shipment_by_id(db: Session, id: int):
|
||||
logging.info(f"Fetching shipment with ID: {id}")
|
||||
shipment = db.query(Shipment).options(
|
||||
|
@ -3,7 +3,7 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.routers import address, contact, proposal, dewar, shipment, puck, spreadsheet
|
||||
from app.routers import address, contact, proposal, dewar, shipment, puck, spreadsheet, logistics
|
||||
from app.database import Base, engine, SessionLocal, load_sample_data
|
||||
|
||||
app = FastAPI()
|
||||
@ -39,6 +39,7 @@ app.include_router(dewar.router, prefix="/dewars", tags=["dewars"])
|
||||
app.include_router(shipment.router, prefix="/shipments", tags=["shipments"])
|
||||
app.include_router(puck.router, prefix="/pucks", tags=["pucks"])
|
||||
app.include_router(spreadsheet.router, tags=["spreadsheet"])
|
||||
app.include_router(dewar.router, prefix="/logistics", tags=["logistics"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
@ -1,6 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Date, ForeignKey, JSON
|
||||
from sqlalchemy import Column, Integer, String, Date, ForeignKey, JSON, Interval, DateTime, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.database import Base
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
|
||||
|
||||
@ -130,3 +131,23 @@ class Sample(Base):
|
||||
# Foreign keys and relationships
|
||||
puck_id = Column(Integer, ForeignKey('pucks.id'))
|
||||
puck = relationship("Puck", back_populates="samples")
|
||||
|
||||
class Slot(Base):
|
||||
__tablename__ = "slots"
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
occupied = Column(Boolean, default=False)
|
||||
needs_refill = Column(Boolean, default=False)
|
||||
time_until_refill = Column(Interval, nullable=True)
|
||||
last_refill = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class LogisticsEvent(Base):
|
||||
__tablename__ = "logistics_events"
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
dewar_id = Column(Integer, ForeignKey('dewars.id'), nullable=False)
|
||||
slot_id = Column(String, ForeignKey('slots.id'), nullable=True)
|
||||
event_type = Column(String, nullable=False)
|
||||
timestamp = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
dewar = relationship("Dewar")
|
||||
slot = relationship("Slot")
|
2084
backend/app/psi_01_lp.svg
Normal file
2084
backend/app/psi_01_lp.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 516 KiB |
BIN
backend/app/routers/Heidi-logo.png
Normal file
BIN
backend/app/routers/Heidi-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 579 KiB |
@ -1,3 +1,6 @@
|
||||
import os
|
||||
import tempfile # <-- Add this import
|
||||
import xml.etree.ElementTree as ET
|
||||
from fastapi import APIRouter, HTTPException, status, Depends, Response
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from typing import List
|
||||
@ -11,28 +14,30 @@ from app.schemas import (
|
||||
DewarType as DewarTypeSchema,
|
||||
DewarTypeCreate,
|
||||
DewarSerialNumber as DewarSerialNumberSchema,
|
||||
DewarSerialNumberCreate
|
||||
DewarSerialNumberCreate,
|
||||
Shipment as ShipmentSchema # Clearer name for schema
|
||||
)
|
||||
from app.models import (
|
||||
Dewar as DewarModel,
|
||||
Puck as PuckModel,
|
||||
Sample as SampleModel,
|
||||
DewarType as DewarTypeModel,
|
||||
DewarSerialNumber as DewarSerialNumberModel
|
||||
DewarSerialNumber as DewarSerialNumberModel,
|
||||
Shipment as ShipmentModel # Clearer name for model
|
||||
)
|
||||
from app.dependencies import get_db
|
||||
import uuid
|
||||
import qrcode
|
||||
import io
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
from reportlab.lib.pagesizes import A5
|
||||
from PIL import ImageFont, ImageDraw, Image
|
||||
from reportlab.lib.pagesizes import A5, landscape
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.pdfgen import canvas
|
||||
from app.crud import get_shipments, get_shipment_by_id # Import CRUD functions for shipment
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def generate_unique_id(db: Session) -> str:
|
||||
while True:
|
||||
unique_id = str(uuid.uuid4())
|
||||
@ -118,52 +123,113 @@ async def generate_dewar_qrcode(dewar_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
def generate_label(dewar):
|
||||
buffer = BytesIO()
|
||||
c = canvas.Canvas(buffer, pagesize=A5)
|
||||
# Set page orientation to landscape
|
||||
c = canvas.Canvas(buffer, pagesize=landscape(A5))
|
||||
|
||||
# Draw header
|
||||
# Dimensions for the A5 landscape
|
||||
page_width, page_height = landscape(A5)
|
||||
|
||||
# Path to the PNG logo
|
||||
file_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
png_logo_path = os.path.join(file_dir, "Heidi-logo.png")
|
||||
|
||||
# Open the logo with PIL to get its size
|
||||
logo_image = Image.open(png_logo_path)
|
||||
logo_aspect_ratio = logo_image.width / logo_image.height # original aspect ratio
|
||||
|
||||
# Desired logo width in the PDF (you can adjust this size)
|
||||
desired_logo_width = 4 * cm
|
||||
desired_logo_height = desired_logo_width / logo_aspect_ratio # maintain aspect ratio
|
||||
|
||||
# Draw header text
|
||||
c.setFont("Helvetica-Bold", 16)
|
||||
c.drawCentredString(10.5 * cm, 14 * cm, "COMPANY LOGO / TITLE")
|
||||
c.drawString(2 * cm, page_height - 2 * cm, "Paul Scherrer Institut")
|
||||
|
||||
# Draw the Heidi logo with preserved aspect ratio
|
||||
c.drawImage(png_logo_path, page_width - desired_logo_width - 2 * cm,
|
||||
page_height - desired_logo_height - 2 * cm,
|
||||
width=desired_logo_width, height=desired_logo_height, mask='auto')
|
||||
|
||||
# Draw details section
|
||||
c.setFont("Helvetica", 12)
|
||||
c.drawString(2 * cm, 12.5 * cm, f"Dewar Name: {dewar.dewar_name}")
|
||||
c.drawString(2 * cm, 11.5 * cm, f"Unique ID: {dewar.unique_id}")
|
||||
if dewar.dewar_type:
|
||||
c.drawString(2 * cm, 10.5 * cm, f"Dewar Type: {dewar.dewar_type.dewar_type}")
|
||||
else:
|
||||
c.drawString(2 * cm, 10.5 * cm, "Dewar Type: Unknown")
|
||||
c.drawString(2 * cm, 9.5 * cm, "Beamtime Information: Placeholder")
|
||||
|
||||
y_position = page_height - 4 * cm # Adjusted to ensure text doesn't overlap with the logo
|
||||
line_height = 0.8 * cm
|
||||
|
||||
if dewar.shipment:
|
||||
c.drawString(2 * cm, y_position, f"Shipment Name: {dewar.shipment.shipment_name}")
|
||||
y_position -= line_height
|
||||
|
||||
c.drawString(2 * cm, y_position, f"Dewar Name: {dewar.dewar_name}")
|
||||
y_position -= line_height
|
||||
|
||||
c.drawString(2 * cm, y_position, f"Unique ID: {dewar.unique_id}")
|
||||
y_position -= line_height
|
||||
|
||||
if dewar.contact_person:
|
||||
contact_person = dewar.contact_person
|
||||
c.drawString(2 * cm, y_position, f"Contact: {contact_person.firstname} {contact_person.lastname}")
|
||||
y_position -= line_height
|
||||
c.drawString(2 * cm, y_position, f"Email: {contact_person.email}")
|
||||
y_position -= line_height
|
||||
c.drawString(2 * cm, y_position, f"Phone: {contact_person.phone_number}")
|
||||
y_position -= line_height
|
||||
|
||||
if dewar.return_address:
|
||||
return_address = dewar.return_address
|
||||
c.drawString(2 * cm, y_position, f"Return Address: {return_address.street}")
|
||||
y_position -= line_height
|
||||
c.drawString(2 * cm, y_position, f"City: {return_address.city}")
|
||||
y_position -= line_height
|
||||
c.drawString(2 * cm, y_position, f"Postal Code: {return_address.zipcode}")
|
||||
y_position -= line_height
|
||||
c.drawString(2 * cm, y_position, f"Country: {return_address.country}")
|
||||
y_position -= line_height
|
||||
|
||||
c.drawString(2 * cm, y_position, f"Beamtime Information: Placeholder")
|
||||
|
||||
# Generate QR code
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=4)
|
||||
qr.add_data(dewar.unique_id)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill='black', back_color='white')
|
||||
qr_io = BytesIO()
|
||||
img.save(qr_io, format='PNG')
|
||||
qr_io.seek(0)
|
||||
qr_image = Image.open(qr_io)
|
||||
qr_img = qr.make_image(fill='black', back_color='white').convert("RGBA")
|
||||
|
||||
# Save this QR code to a temporary file
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
|
||||
qr_img.save(temp_file, format='PNG')
|
||||
temp_file_path = temp_file.name
|
||||
|
||||
# Add QR code to PDF
|
||||
c.drawInlineImage(qr_image, 8 * cm, 5 * cm, width=4 * cm, height=4 * cm)
|
||||
c.drawImage(temp_file_path, page_width - 6 * cm, 5 * cm, width=4 * cm, height=4 * cm)
|
||||
|
||||
# Add footer text
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawCentredString(10.5 * cm, 4 * cm, "Scan for more information")
|
||||
c.drawCentredString(page_width / 2, 2 * cm, "Scan for more information")
|
||||
|
||||
# Draw border
|
||||
c.rect(1 * cm, 3 * cm, 18 * cm, 12 * cm)
|
||||
c.setLineWidth(1)
|
||||
c.rect(1 * cm, 1 * cm, page_width - 2 * cm, page_height - 2 * cm) # Adjusted dimensions
|
||||
|
||||
# Finalize the canvas
|
||||
c.showPage()
|
||||
c.save()
|
||||
|
||||
buffer.seek(0)
|
||||
|
||||
# Cleanup temporary file
|
||||
os.remove(temp_file_path)
|
||||
|
||||
return buffer
|
||||
|
||||
@router.get("/{dewar_id}/download-label", response_class=Response)
|
||||
async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)):
|
||||
dewar = db.query(DewarModel).options(joinedload(DewarModel.dewar_type)).filter(DewarModel.id == dewar_id).first()
|
||||
dewar = db.query(DewarModel).options(
|
||||
joinedload(DewarModel.pucks).joinedload(PuckModel.samples),
|
||||
joinedload(DewarModel.contact_person),
|
||||
joinedload(DewarModel.return_address),
|
||||
joinedload(DewarModel.shipment)
|
||||
).filter(DewarModel.id == dewar_id).first()
|
||||
|
||||
if not dewar:
|
||||
raise HTTPException(status_code=404, detail="Dewar not found")
|
||||
if not dewar.unique_id:
|
||||
@ -220,7 +286,10 @@ def get_all_serial_numbers(db: Session = Depends(get_db)):
|
||||
@router.get("/{dewar_id}", response_model=DewarSchema)
|
||||
async def get_dewar(dewar_id: int, db: Session = Depends(get_db)):
|
||||
dewar = db.query(DewarModel).options(
|
||||
joinedload(DewarModel.pucks).joinedload(PuckModel.samples)
|
||||
joinedload(DewarModel.pucks).joinedload(PuckModel.samples),
|
||||
joinedload(DewarModel.contact_person),
|
||||
joinedload(DewarModel.return_address),
|
||||
joinedload(DewarModel.shipment)
|
||||
).filter(DewarModel.id == dewar_id).first()
|
||||
|
||||
if not dewar:
|
||||
@ -252,4 +321,25 @@ async def delete_dewar(dewar_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
db.delete(dewar)
|
||||
db.commit()
|
||||
return
|
||||
return
|
||||
|
||||
# New routes for shipments
|
||||
@router.get("/shipments", response_model=List[ShipmentSchema])
|
||||
async def get_all_shipments(db: Session = Depends(get_db)):
|
||||
try:
|
||||
shipments = get_shipments(db)
|
||||
return shipments
|
||||
except SQLAlchemyError as e:
|
||||
logging.error(f"Database error occurred: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.get("/shipments/{id}", response_model=ShipmentSchema)
|
||||
async def get_single_shipment(id: int, db: Session = Depends(get_db)):
|
||||
try:
|
||||
shipment = get_shipment_by_id(db, id)
|
||||
if shipment is None:
|
||||
raise HTTPException(status_code=404, detail="Shipment not found")
|
||||
return shipment
|
||||
except SQLAlchemyError as e:
|
||||
logging.error(f"Database error occurred: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
67
backend/app/routers/logistics.py
Normal file
67
backend/app/routers/logistics.py
Normal file
@ -0,0 +1,67 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
|
||||
from app.models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel
|
||||
from app.schemas import LogisticsEventCreate, SlotCreate, Slot as SlotSchema
|
||||
from app.database import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/scan-dewar")
|
||||
async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)):
|
||||
dewar_qr_code = event_data.dewar_qr_code
|
||||
location_qr_code = event_data.location_qr_code
|
||||
transaction_type = event_data.transaction_type
|
||||
|
||||
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
|
||||
if not dewar:
|
||||
raise HTTPException(status_code=404, detail="Dewar not found")
|
||||
|
||||
if transaction_type == 'incoming':
|
||||
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
|
||||
if not slot or slot.occupied:
|
||||
raise HTTPException(status_code=404, detail="Slot not found or already occupied")
|
||||
slot.occupied = True
|
||||
log_event(db, dewar.id, slot.id, 'incoming')
|
||||
|
||||
elif transaction_type == 'beamline':
|
||||
log_event(db, dewar.id, None, 'beamline')
|
||||
|
||||
elif transaction_type == 'outgoing':
|
||||
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
|
||||
if not slot or not slot.occupied:
|
||||
raise HTTPException(status_code=404, detail="Slot not found or not occupied")
|
||||
slot.occupied = False
|
||||
log_event(db, dewar.id, slot.id, 'outgoing')
|
||||
|
||||
elif transaction_type == 'release':
|
||||
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
|
||||
if not slot or not slot.occupied:
|
||||
raise HTTPException(status_code=404, detail="Slot not found or not occupied")
|
||||
slot.occupied = False
|
||||
log_event(db, dewar.id, slot.id, 'released')
|
||||
|
||||
db.commit()
|
||||
return {"message": "Status updated successfully"}
|
||||
|
||||
def log_event(db: Session, dewar_id: int, slot_id: int, event_type: str):
|
||||
new_event = LogisticsEventModel(dewar_id=dewar_id, slot_id=slot_id, event_type=event_type)
|
||||
db.add(new_event)
|
||||
|
||||
@router.get("/refill-status", response_model=List[SlotSchema])
|
||||
async def refill_status(db: Session = Depends(get_db)):
|
||||
slots_needing_refill = db.query(SlotModel).filter(SlotModel.needs_refill == True).all()
|
||||
result = []
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
for slot in slots_needing_refill:
|
||||
time_until_next_refill = slot.last_refill + timedelta(hours=24) - current_time
|
||||
result.append({
|
||||
'slot_id': slot.id,
|
||||
'needs_refill': slot.needs_refill,
|
||||
'time_until_refill': str(time_until_next_refill)
|
||||
})
|
||||
|
||||
return result
|
@ -1,4 +1,5 @@
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta # Add this import
|
||||
from pydantic import BaseModel, EmailStr, constr, Field
|
||||
from datetime import date
|
||||
|
||||
@ -270,4 +271,23 @@ class ShipmentCreate(BaseModel):
|
||||
|
||||
|
||||
class UpdateShipmentComments(BaseModel):
|
||||
comments: str
|
||||
comments: str
|
||||
|
||||
class LogisticsEventCreate(BaseModel):
|
||||
dewar_qr_code: str
|
||||
location_qr_code: str
|
||||
transaction_type: str
|
||||
|
||||
class SlotCreate(BaseModel):
|
||||
id: int
|
||||
needs_refill: bool
|
||||
last_refill: datetime
|
||||
occupied: bool
|
||||
|
||||
class Slot(BaseModel):
|
||||
slot_id: int
|
||||
needs_refill: bool
|
||||
time_until_refill: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
Loading…
x
Reference in New Issue
Block a user