import json from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from typing import Optional from database import get_db from models.waste import WasteLog from models.product import Product from models.business_day import BusinessDay from models.user import User from schemas.waste import WasteLogCreate, WasteLogOut, VALID_REASONS, REASON_LABELS from routers.deps import require_manager router = APIRouter() def _effective_cost(product: Product) -> Optional[float]: """Return product's current effective cost (breakdown sum or simple), or None.""" if product.cost_breakdown: try: entries = json.loads(product.cost_breakdown) total = sum(e.get("amount", 0.0) for e in entries if isinstance(e, dict)) if total > 0: return total except Exception: pass if product.cost_simple: return product.cost_simple return None def _waste_out(w: WasteLog) -> dict: product_name = w.product.name if w.product else f"#{w.product_id}" logger_name = None if w.logged_by: logger_name = w.logged_by.full_name or w.logged_by.username return { "id": w.id, "product_id": w.product_id, "product_name": product_name, "quantity": w.quantity, "unit_cost_snapshot": w.unit_cost_snapshot, "total_cost": w.total_cost, "reason": w.reason, "reason_label": REASON_LABELS.get(w.reason, w.reason), "reason_notes": w.reason_notes, "logged_by_id": w.logged_by_id, "logged_by_name": logger_name, "business_day_id": w.business_day_id, "logged_at": w.logged_at, } @router.get("/") def list_waste( business_day_id: Optional[int] = None, from_dt: Optional[str] = Query(default=None, alias="from"), to_dt: Optional[str] = Query(default=None, alias="to"), db: Session = Depends(get_db), user: User = Depends(require_manager), ): from datetime import datetime q = db.query(WasteLog).order_by(WasteLog.logged_at.desc()) if business_day_id: q = q.filter(WasteLog.business_day_id == business_day_id) elif from_dt and to_dt: q = q.filter( WasteLog.logged_at >= datetime.fromisoformat(from_dt), WasteLog.logged_at <= datetime.fromisoformat(to_dt), ) entries = q.all() return {"entries": [_waste_out(w) for w in entries]} @router.post("/", status_code=status.HTTP_201_CREATED) def log_waste( body: WasteLogCreate, db: Session = Depends(get_db), user: User = Depends(require_manager), ): if body.reason not in VALID_REASONS: raise HTTPException(status_code=400, detail=f"Invalid reason. Valid: {sorted(VALID_REASONS)}") if body.quantity <= 0: raise HTTPException(status_code=400, detail="Quantity must be positive") product = db.query(Product).filter(Product.id == body.product_id).first() if not product: raise HTTPException(status_code=404, detail="Product not found") # Snapshot current effective cost unit_cost = _effective_cost(product) total_cost = round(unit_cost * body.quantity, 2) if unit_cost is not None else None # Attach to open business day if any active_day = db.query(BusinessDay).filter(BusinessDay.status == "open").first() entry = WasteLog( product_id=body.product_id, quantity=body.quantity, reason=body.reason, reason_notes=body.reason_notes, unit_cost_snapshot=unit_cost, total_cost=total_cost, logged_by_id=user.id, business_day_id=active_day.id if active_day else None, ) db.add(entry) db.commit() db.refresh(entry) return _waste_out(entry) @router.delete("/{entry_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_waste( entry_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager), ): entry = db.query(WasteLog).filter(WasteLog.id == entry_id).first() if not entry: raise HTTPException(status_code=404, detail="Waste entry not found") # Only allow deletion within the same business day active_day = db.query(BusinessDay).filter(BusinessDay.status == "open").first() if active_day and entry.business_day_id != active_day.id: raise HTTPException(status_code=403, detail="Can only delete waste entries from the current open business day") db.delete(entry) db.commit() @router.get("/summary") def waste_summary( business_day_id: Optional[int] = None, from_dt: Optional[str] = Query(default=None, alias="from"), to_dt: Optional[str] = Query(default=None, alias="to"), db: Session = Depends(get_db), user: User = Depends(require_manager), ): from datetime import datetime q = db.query(WasteLog) if business_day_id: q = q.filter(WasteLog.business_day_id == business_day_id) elif from_dt and to_dt: q = q.filter( WasteLog.logged_at >= datetime.fromisoformat(from_dt), WasteLog.logged_at <= datetime.fromisoformat(to_dt), ) entries = q.all() total_items = len(entries) total_cost = round(sum(e.total_cost for e in entries if e.total_cost is not None), 2) uncosted_count = sum(1 for e in entries if e.total_cost is None) by_reason: dict = {} by_product: dict = {} for e in entries: r = e.reason if r not in by_reason: by_reason[r] = {"reason": r, "label": REASON_LABELS.get(r, r), "count": 0, "total_cost": 0.0} by_reason[r]["count"] += 1 by_reason[r]["total_cost"] += e.total_cost or 0.0 pid = e.product_id pname = e.product.name if e.product else f"#{pid}" if pid not in by_product: by_product[pid] = {"product_id": pid, "product_name": pname, "qty": 0.0, "total_cost": 0.0} by_product[pid]["qty"] += e.quantity by_product[pid]["total_cost"] += e.total_cost or 0.0 for v in by_reason.values(): v["total_cost"] = round(v["total_cost"], 2) for v in by_product.values(): v["total_cost"] = round(v["total_cost"], 2) return { "total_items": total_items, "total_cost": total_cost, "uncosted_count": uncosted_count, "by_reason": sorted(by_reason.values(), key=lambda x: -x["count"]), "by_product": sorted(by_product.values(), key=lambda x: -x["total_cost"]), }