Reporting and batch management
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user