Reporting and batch management

This commit is contained in:
2026-04-06 11:04:06 -04:00
parent b958ca493b
commit 36f0a5b07e
5 changed files with 392 additions and 36 deletions
+115
View File
@@ -245,6 +245,7 @@ class DispensingCreate(BaseModel):
drug_variant_id: int
quantity: Optional[float] = None
dispense_mode: str = "subunit"
dispense_source: str = "batch"
requested_pack_id: Optional[int] = None
requested_pack_count: Optional[float] = None
animal_name: Optional[str] = None
@@ -496,12 +497,16 @@ def resolve_requested_allocations(
requested_quantity: float,
requested_allocations: List[DispensingAllocationCreate],
dispense_mode: str,
dispense_source: str,
requested_pack_id: Optional[int],
) -> List[Dict[str, Any]]:
"""Validate explicit batch allocations against in-date stock for the variant."""
today = date.today()
total_batched_quantity = sum(float(batch.quantity or 0) for batch in db.query(Batch).filter(Batch.drug_variant_id == variant_id).all())
legacy_unbatched_quantity = max(0.0, float(variant_quantity or 0) - total_batched_quantity)
selected_source = (dispense_source or "batch").strip().lower()
if selected_source not in {"batch", "legacy"}:
raise HTTPException(status_code=400, detail="dispense_source must be either 'batch' or 'legacy'")
eligible_batches = (
db.query(Batch)
.filter(
@@ -513,6 +518,20 @@ def resolve_requested_allocations(
.all()
)
if selected_source == "legacy":
if dispense_mode == "pack":
raise HTTPException(status_code=400, detail="Whole-pack dispensing requires batched stock with pack information")
if requested_allocations:
raise HTTPException(status_code=400, detail="Batch allocations cannot be supplied when dispensing legacy stock")
if legacy_unbatched_quantity <= 0:
raise HTTPException(status_code=400, detail="No legacy loose stock is available for this variant")
if requested_quantity - legacy_unbatched_quantity > 1e-6:
raise HTTPException(
status_code=400,
detail=f"Insufficient unbatched stock. Available: {legacy_unbatched_quantity}, Requested: {requested_quantity}",
)
return []
if not eligible_batches:
if dispense_mode == "pack":
raise HTTPException(status_code=400, detail="Whole-pack dispensing requires batched stock with pack information")
@@ -1359,9 +1378,12 @@ def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db), c
requested_quantity=dispense_qty,
requested_allocations=dispensing.allocations,
dispense_mode=dispense_mode,
dispense_source=dispensing.dispense_source,
requested_pack_id=resolved["pack_id"],
)
selected_source = (dispensing.dispense_source or ("legacy" if not allocations else "batch")).strip().lower()
user_name = dispensing.user_name or current_user.username
primary_batch_id = allocations[0]["batch"].id if allocations else None
@@ -1402,6 +1424,7 @@ def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db), c
"drug_variant_id": dispensing.drug_variant_id,
"requested_quantity": dispense_qty,
"dispense_mode": dispense_mode,
"dispense_source": selected_source,
"requested_pack_id": resolved["pack_id"],
"requested_pack_count": resolved["pack_count"],
"allocations": allocation_payload,
@@ -2054,6 +2077,98 @@ def report_stock_by_location(
return result
@router.get("/reports/global-inventory")
def report_global_inventory(
format: str = "json",
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
variant_rows = (
db.query(DrugVariant, Drug)
.join(Drug, DrugVariant.drug_id == Drug.id)
.order_by(Drug.name.asc(), DrugVariant.strength.asc())
.all()
)
result: List[Dict[str, Any]] = []
for variant, drug in variant_rows:
batch_rows = (
db.query(Batch, Location)
.join(Location, Batch.location_id == Location.id)
.filter(Batch.drug_variant_id == variant.id, Batch.quantity > 0)
.order_by(Batch.expiry_date.asc(), Location.name.asc(), Batch.batch_number.asc())
.all()
)
total_batch_quantity = 0.0
for batch, location in batch_rows:
total_batch_quantity += float(batch.quantity or 0)
result.append(
{
"batch_id": batch.id,
"batch_number": batch.batch_number,
"drug_name": drug.name,
"strength": variant.strength,
"quantity": batch.quantity,
"unit": variant.unit,
"location_name": location.name,
"expiry_date": batch.expiry_date,
"inventory_source": "batch",
"is_controlled": bool(drug.is_controlled),
}
)
legacy_quantity = max(0.0, float(variant.quantity or 0) - total_batch_quantity)
if legacy_quantity > 1e-6:
result.append(
{
"batch_id": None,
"batch_number": "Legacy stock",
"drug_name": drug.name,
"strength": variant.strength,
"quantity": legacy_quantity,
"unit": variant.unit,
"location_name": None,
"expiry_date": None,
"inventory_source": "legacy",
"is_controlled": bool(drug.is_controlled),
}
)
if format.lower() == "csv":
csv_rows = [
[
item["drug_name"],
item["strength"],
item["batch_number"],
item["quantity"],
item["unit"],
item["location_name"],
item["expiry_date"],
item["inventory_source"],
item["is_controlled"],
]
for item in result
]
return _csv_response(
"global_inventory.csv",
[
"drug_name",
"strength",
"batch_number",
"quantity",
"unit",
"location_name",
"expiry_date",
"inventory_source",
"is_controlled",
],
csv_rows,
)
return result
@router.get("/reports/batch-attention")
def report_batch_attention(
format: str = "json",