Disposal
This commit is contained in:
+147
-1
@@ -73,6 +73,9 @@ class UserCreate(BaseModel):
|
||||
password: str
|
||||
role: Optional[str] = "user" # admin, user, readonly
|
||||
|
||||
class UserRoleUpdate(BaseModel):
|
||||
role: str
|
||||
|
||||
class PasswordChange(BaseModel):
|
||||
current_password: str
|
||||
new_password: str
|
||||
@@ -297,6 +300,17 @@ class DispensingAllocationCreate(BaseModel):
|
||||
quantity: float
|
||||
|
||||
|
||||
class InventoryDisposeRequest(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
|
||||
notes: Optional[str] = None
|
||||
allocations: List[DispensingAllocationCreate] = []
|
||||
|
||||
|
||||
class DispensingCreate(BaseModel):
|
||||
drug_variant_id: int
|
||||
quantity: Optional[float] = None
|
||||
@@ -876,6 +890,34 @@ def delete_user(user_id: int, db: Session = Depends(get_db), current_user: User
|
||||
db.commit()
|
||||
return {"message": "User deleted successfully"}
|
||||
|
||||
@router.patch("/users/{user_id}/role", response_model=UserResponse)
|
||||
def update_user_role(user_id: int, role_data: UserRoleUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
|
||||
"""Update a user's role (admin only)"""
|
||||
if current_user.id == user_id:
|
||||
raise HTTPException(status_code=400, detail="Cannot change your own role")
|
||||
|
||||
valid_roles = ["admin", "user", "readonly"]
|
||||
if role_data.role not in valid_roles:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid role. Must be one of: {', '.join(valid_roles)}")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
old_role = user.role
|
||||
user.role = role_data.role
|
||||
write_audit_log(
|
||||
db,
|
||||
action="user.role.update",
|
||||
entity_type="user",
|
||||
entity_id=user.id,
|
||||
actor=current_user,
|
||||
details={"username": user.username, "old_role": old_role, "new_role": user.role},
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
@router.post("/auth/change-password")
|
||||
def change_own_password(password_data: PasswordChange, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Change current user's password"""
|
||||
@@ -1620,6 +1662,110 @@ def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db), c
|
||||
"allocations": allocation_payload,
|
||||
}
|
||||
|
||||
@router.post("/dispose")
|
||||
def dispose_inventory(payload: InventoryDisposeRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
|
||||
"""Dispose selected inventory and reduce stock (admin only)"""
|
||||
variant = db.query(DrugVariant).filter(DrugVariant.id == payload.drug_variant_id).first()
|
||||
if not variant:
|
||||
raise HTTPException(status_code=404, detail="Drug variant not found")
|
||||
|
||||
dispose_mode = (payload.dispense_mode or "subunit").strip().lower()
|
||||
if dispose_mode not in {"subunit", "pack"}:
|
||||
raise HTTPException(status_code=400, detail="dispose_mode must be either 'subunit' or 'pack'")
|
||||
|
||||
if dispose_mode == "pack":
|
||||
if payload.requested_pack_id is None or payload.requested_pack_count is None:
|
||||
raise HTTPException(status_code=400, detail="Pack disposal requires requested_pack_id and requested_pack_count")
|
||||
if payload.requested_pack_count <= 0:
|
||||
raise HTTPException(status_code=400, detail="Pack count must be greater than zero")
|
||||
if abs(payload.requested_pack_count - round(payload.requested_pack_count)) > 1e-6:
|
||||
raise HTTPException(status_code=400, detail="Whole-pack disposal requires an integer pack count")
|
||||
|
||||
resolved = resolve_pack_quantity(
|
||||
db,
|
||||
variant_id=variant.id,
|
||||
quantity=None,
|
||||
pack_id=payload.requested_pack_id,
|
||||
pack_count=payload.requested_pack_count,
|
||||
)
|
||||
else:
|
||||
if payload.quantity is None or payload.quantity <= 0:
|
||||
raise HTTPException(status_code=400, detail="Quantity disposal requires quantity > 0")
|
||||
resolved = resolve_pack_quantity(
|
||||
db,
|
||||
variant_id=variant.id,
|
||||
quantity=payload.quantity,
|
||||
pack_id=None,
|
||||
pack_count=None,
|
||||
)
|
||||
|
||||
dispose_qty = resolved["quantity"]
|
||||
if variant.quantity < dispose_qty:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Insufficient quantity. Available: {variant.quantity}, Requested: {dispose_qty}",
|
||||
)
|
||||
|
||||
allocations = resolve_requested_allocations(
|
||||
db,
|
||||
variant_id=variant.id,
|
||||
variant_quantity=variant.quantity,
|
||||
requested_quantity=dispose_qty,
|
||||
requested_allocations=payload.allocations,
|
||||
dispense_mode=dispose_mode,
|
||||
dispense_source=payload.dispense_source,
|
||||
requested_pack_id=resolved["pack_id"],
|
||||
)
|
||||
|
||||
selected_source = (payload.dispense_source or ("legacy" if not allocations else "batch")).strip().lower()
|
||||
allocation_payload = []
|
||||
disposed_at = datetime.utcnow()
|
||||
|
||||
for allocation in allocations:
|
||||
batch = allocation["batch"]
|
||||
qty = allocation["quantity"]
|
||||
remaining_before = batch.quantity
|
||||
batch.quantity -= qty
|
||||
if batch.quantity <= 1e-6:
|
||||
batch.quantity = 0
|
||||
batch.disposed_at = disposed_at
|
||||
batch.disposed_by_user_id = current_user.id
|
||||
batch.disposed_quantity = remaining_before
|
||||
batch.disposal_notes = payload.notes
|
||||
recompute_batch_pack_state(batch)
|
||||
allocation_payload.append({"batch_id": batch.id, "quantity": qty})
|
||||
|
||||
variant.quantity = max(0, variant.quantity - dispose_qty)
|
||||
|
||||
write_audit_log(
|
||||
db,
|
||||
action="inventory.dispose",
|
||||
entity_type="drug_variant",
|
||||
entity_id=variant.id,
|
||||
actor=current_user,
|
||||
details={
|
||||
"drug_variant_id": variant.id,
|
||||
"disposed_quantity": dispose_qty,
|
||||
"dispose_mode": dispose_mode,
|
||||
"dispose_source": selected_source,
|
||||
"requested_pack_id": resolved["pack_id"],
|
||||
"requested_pack_count": resolved["pack_count"],
|
||||
"allocations": allocation_payload,
|
||||
"notes": payload.notes,
|
||||
},
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(variant)
|
||||
return {
|
||||
"message": "Inventory disposed successfully",
|
||||
"drug_variant_id": variant.id,
|
||||
"disposed_quantity": dispose_qty,
|
||||
"dispose_mode": dispose_mode,
|
||||
"dispose_source": selected_source,
|
||||
"allocations": allocation_payload,
|
||||
}
|
||||
|
||||
@router.get("/dispense/history", response_model=List[DispensingResponse])
|
||||
def list_dispensings(skip: int = 0, limit: int = 100, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Get dispensing records (audit log)"""
|
||||
@@ -2012,7 +2158,7 @@ def update_batch(batch_id: int, payload: BatchUpdate, db: Session = Depends(get_
|
||||
|
||||
|
||||
@router.post("/batches/{batch_id}/dispose", response_model=BatchResponse)
|
||||
def dispose_batch(batch_id: int, payload: BatchDisposeRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_non_readonly_user)):
|
||||
def dispose_batch(batch_id: int, payload: BatchDisposeRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
|
||||
batch = db.query(Batch).filter(Batch.id == batch_id).first()
|
||||
if not batch:
|
||||
raise HTTPException(status_code=404, detail="Batch not found")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
python-jose[cryptography]==3.3.0
|
||||
fastapi==0.137.2
|
||||
uvicorn==0.49.0
|
||||
sqlalchemy==2.0.51
|
||||
pydantic==2.13.4
|
||||
python-multipart==0.0.32
|
||||
python-jose[cryptography]==3.5.0
|
||||
passlib[argon2]==1.7.4
|
||||
paho-mqtt==1.6.1
|
||||
|
||||
Reference in New Issue
Block a user