Barcode scanning and GTIN mapping

This commit is contained in:
2026-04-16 15:32:36 -04:00
parent 2aeba2f563
commit cfb08bd288
6 changed files with 938 additions and 75 deletions
+119 -24
View File
@@ -18,11 +18,13 @@ from .models import (
Batch,
AuditLog,
User,
GtinMapping,
)
from .auth import hash_password, verify_password, create_access_token, get_current_user, get_current_admin_user, get_current_non_readonly_user, ACCESS_TOKEN_EXPIRE_MINUTES
from .mqtt_service import publish_label_print_with_response
from .migrate_to_roles import migrate_users_table
from .migrate_compliance import migrate_compliance_schema
from .migrate_gtin import migrate_gtin_schema
from pydantic import BaseModel
# Run migration to convert is_admin to role
@@ -39,6 +41,11 @@ except Exception as e:
# Create tables
Base.metadata.create_all(bind=engine)
try:
migrate_gtin_schema()
except Exception as e:
print(f"Warning: GTIN migration failed: {e}. Continuing anyway...")
# Seed default locations after table creation.
try:
migrate_compliance_schema()
@@ -138,14 +145,37 @@ class BatchDisposeRequest(BaseModel):
notes: Optional[str] = None
class GtinMappingCreate(BaseModel):
gtin: str
drug_variant_id: int
variant_pack_id: int
class GtinMappingResponse(BaseModel):
id: int
gtin: str
drug_variant_id: int
variant_pack_id: int
drug_id: int
drug_name: str
variant_strength: str
variant_unit: str
pack_label: str
pack_size_in_base_units: float
pack_unit_name: str
class Config:
from_attributes = True
class BatchResponse(BaseModel):
id: int
drug_variant_id: int
batch_number: str
quantity: float
received_pack_id: Optional[int] = None
received_pack_label: Optional[str] = None
received_pack_count: Optional[float] = None
received_pack_unit_name: Optional[str] = None
received_pack_size_snapshot: Optional[float] = None
current_full_pack_count: Optional[float] = None
current_loose_base_units: Optional[float] = None
@@ -187,14 +217,12 @@ class DrugVariantUpdate(BaseModel):
class VariantPackCreate(BaseModel):
label: str
pack_unit_name: str
pack_size_in_base_units: float
is_active: bool = True
class VariantPackUpdate(BaseModel):
label: Optional[str] = None
pack_unit_name: Optional[str] = None
pack_size_in_base_units: Optional[float] = None
is_active: Optional[bool] = None
@@ -203,7 +231,6 @@ class VariantPackUpdate(BaseModel):
class VariantPackResponse(BaseModel):
id: int
drug_variant_id: int
label: str
pack_unit_name: str
pack_size_in_base_units: float
is_active: bool
@@ -383,7 +410,7 @@ def serialize_batch_response(db: Session, batch: Batch) -> Dict[str, Any]:
"batch_number": batch.batch_number,
"quantity": batch.quantity,
"received_pack_id": batch.received_pack_id,
"received_pack_label": pack.label if pack else None,
"received_pack_unit_name": pack.pack_unit_name if pack else None,
"received_pack_count": batch.received_pack_count,
"received_pack_size_snapshot": batch.received_pack_size_snapshot,
"current_full_pack_count": batch.current_full_pack_count,
@@ -429,7 +456,6 @@ def serialize_variant_pack(pack: VariantPack) -> Dict[str, Any]:
return {
"id": pack.id,
"drug_variant_id": pack.drug_variant_id,
"label": pack.label,
"pack_unit_name": pack.pack_unit_name,
"pack_size_in_base_units": pack.pack_size_in_base_units,
"is_active": pack.is_active,
@@ -1050,15 +1076,12 @@ def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session =
db.flush()
# Ensure each variant has at least one active default 1:1 pack representation.
db.add(
VariantPack(
db.add(VariantPack(
drug_variant_id=db_variant.id,
label=f"1 {base_unit}",
pack_unit_name=base_unit,
pack_size_in_base_units=1,
is_active=True,
)
)
))
write_audit_log(
db,
@@ -1232,10 +1255,7 @@ def create_variant_pack(
if not variant:
raise HTTPException(status_code=404, detail="Drug variant not found")
label = payload.label.strip()
pack_unit_name = payload.pack_unit_name.strip()
if not label:
raise HTTPException(status_code=400, detail="Pack label cannot be empty")
if not pack_unit_name:
raise HTTPException(status_code=400, detail="Pack unit name cannot be empty")
if payload.pack_size_in_base_units <= 0:
@@ -1243,7 +1263,6 @@ def create_variant_pack(
row = VariantPack(
drug_variant_id=variant_id,
label=label,
pack_unit_name=pack_unit_name,
pack_size_in_base_units=payload.pack_size_in_base_units,
is_active=payload.is_active,
@@ -1257,7 +1276,6 @@ def create_variant_pack(
actor=current_user,
details={
"variant_id": variant_id,
"label": label,
"pack_unit_name": pack_unit_name,
"pack_size_in_base_units": payload.pack_size_in_base_units,
"is_active": payload.is_active,
@@ -1281,12 +1299,6 @@ def update_variant_pack(
before = serialize_variant_pack(row)
if payload.label is not None:
cleaned = payload.label.strip()
if not cleaned:
raise HTTPException(status_code=400, detail="Pack label cannot be empty")
row.label = cleaned
if payload.pack_unit_name is not None:
cleaned = payload.pack_unit_name.strip()
if not cleaned:
@@ -2200,7 +2212,7 @@ def report_batch_attention(
"location": location.name,
"expiry_date": batch.expiry_date,
"status": "expired",
"received_pack_label": None,
"received_pack_unit_name": None,
"current_full_pack_count": batch.current_full_pack_count,
"current_loose_base_units": batch.current_loose_base_units,
"is_controlled": bool(drug.is_controlled),
@@ -2450,5 +2462,88 @@ def print_notes(notes_request: NotesPrintRequest, current_user: User = Depends(g
detail=f"Error sending notes print request: {str(e)}"
)
@router.get("/gtin/{gtin}", response_model=GtinMappingResponse)
def get_gtin_mapping(
gtin: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
mapping = db.query(GtinMapping).filter(GtinMapping.gtin == gtin).first()
if not mapping:
raise HTTPException(status_code=404, detail="GTIN not found")
variant = db.query(DrugVariant).filter(DrugVariant.id == mapping.drug_variant_id).first()
drug = db.query(Drug).filter(Drug.id == variant.drug_id).first() if variant else None
pack = db.query(VariantPack).filter(VariantPack.id == mapping.variant_pack_id).first()
if not variant or not drug or not pack:
# Referenced records no longer exist — delete the stale mapping and treat as unknown
db.delete(mapping)
db.commit()
raise HTTPException(status_code=404, detail="GTIN not found")
return GtinMappingResponse(
id=mapping.id,
gtin=mapping.gtin,
drug_variant_id=mapping.drug_variant_id,
variant_pack_id=mapping.variant_pack_id,
drug_id=drug.id,
drug_name=drug.name,
variant_strength=variant.strength,
variant_unit=variant.unit,
pack_label=f"{pack.pack_unit_name} of {int(pack.pack_size_in_base_units) if pack.pack_size_in_base_units == int(pack.pack_size_in_base_units) else pack.pack_size_in_base_units}",
pack_size_in_base_units=pack.pack_size_in_base_units,
pack_unit_name=pack.pack_unit_name,
)
@router.post("/gtin", response_model=GtinMappingResponse, status_code=201)
def create_gtin_mapping(
body: GtinMappingCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_non_readonly_user),
):
existing = db.query(GtinMapping).filter(GtinMapping.gtin == body.gtin).first()
if existing:
raise HTTPException(status_code=409, detail="GTIN mapping already exists")
variant = db.query(DrugVariant).filter(DrugVariant.id == body.drug_variant_id).first()
if not variant:
raise HTTPException(status_code=404, detail="Drug variant not found")
pack = db.query(VariantPack).filter(
VariantPack.id == body.variant_pack_id,
VariantPack.drug_variant_id == body.drug_variant_id,
).first()
if not pack:
raise HTTPException(status_code=404, detail="Pack not found for this variant")
drug = db.query(Drug).filter(Drug.id == variant.drug_id).first()
mapping = GtinMapping(
gtin=body.gtin,
drug_variant_id=body.drug_variant_id,
variant_pack_id=body.variant_pack_id,
created_by_user_id=current_user.id,
)
db.add(mapping)
db.commit()
db.refresh(mapping)
return GtinMappingResponse(
id=mapping.id,
gtin=mapping.gtin,
drug_variant_id=mapping.drug_variant_id,
variant_pack_id=mapping.variant_pack_id,
drug_id=drug.id,
drug_name=drug.name,
variant_strength=variant.strength,
variant_unit=variant.unit,
pack_label=f"{pack.pack_unit_name} of {int(pack.pack_size_in_base_units) if pack.pack_size_in_base_units == int(pack.pack_size_in_base_units) else pack.pack_size_in_base_units}",
pack_size_in_base_units=pack.pack_size_in_base_units,
pack_unit_name=pack.pack_unit_name,
)
# Include router with /api prefix
app.include_router(router)