feat: Phase 2H — void/waste log
Backend:
- New model: WasteLog (waste_log table) — product, quantity (float), reason, reason_notes,
unit_cost_snapshot (copied at log time from product's effective cost), total_cost (stored),
logged_by, business_day_id
- Cost snapshot: same logic as Phase 2A — breakdown sum first, fallback to cost_simple, null if neither
- New router /api/waste/:
- GET / — list entries (?business_day_id= or ?from=&to=), ordered by logged_at desc
- POST / — log waste; snapshots cost; attaches to open business day automatically
- DELETE /{id} — manager only; only allowed within the same open business day
- GET /summary — totals by reason + by product for a period
- product_performance report: waste_qty + waste_cost added per product from the same period;
products with only waste (no sales) also included
- Migration: CREATE TABLE IF NOT EXISTS waste_log
Frontend:
- WastePage (/waste): fast log form (product picker, quantity, reason chips, notes);
today's entries list below (scoped to active business day); cost shown per entry;
delete button for today's entries; history section behind toggle with date-range picker
- ProductPerformance report: Απόβλητα column showing waste_qty + cost in amber
- Sidebar: Trash2 icon for Αποβλήτα (after Καρτέλες)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -904,6 +904,7 @@ def product_performance(
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
from models.product import Product
|
||||
from models.waste import WasteLog
|
||||
|
||||
q = db.query(OrderItem).filter(OrderItem.status.in_(["active", "paid"]))
|
||||
if from_dt:
|
||||
@@ -916,6 +917,24 @@ def product_performance(
|
||||
items = q.all()
|
||||
products_db = {p.id: p for p in db.query(Product).all()}
|
||||
|
||||
# Phase 2H: load waste for the same period
|
||||
wq = db.query(WasteLog)
|
||||
if business_day_id:
|
||||
wq = wq.filter(WasteLog.business_day_id == business_day_id)
|
||||
elif from_dt and to_dt:
|
||||
wq = wq.filter(
|
||||
WasteLog.logged_at >= datetime.fromisoformat(from_dt),
|
||||
WasteLog.logged_at <= datetime.fromisoformat(to_dt),
|
||||
)
|
||||
waste_entries = wq.all()
|
||||
waste_by_product: dict = {}
|
||||
for w in waste_entries:
|
||||
pid = w.product_id
|
||||
if pid not in waste_by_product:
|
||||
waste_by_product[pid] = {"waste_qty": 0.0, "waste_cost": 0.0}
|
||||
waste_by_product[pid]["waste_qty"] += w.quantity
|
||||
waste_by_product[pid]["waste_cost"] += w.total_cost or 0.0
|
||||
|
||||
summary: dict = {}
|
||||
for item in items:
|
||||
pid = item.product_id
|
||||
@@ -945,13 +964,31 @@ def product_performance(
|
||||
summary[pid]["uncosted_revenue"] += revenue
|
||||
summary[pid]["uncosted_items"] += qty
|
||||
|
||||
# Also include products that only have waste (not sold) in the period
|
||||
for pid, wdata in waste_by_product.items():
|
||||
if pid not in summary:
|
||||
product = products_db.get(pid)
|
||||
if category_id and (not product or product.category_id != category_id):
|
||||
continue
|
||||
summary[pid] = {
|
||||
"product_id": pid,
|
||||
"product_name": product.name if product else f"#{pid}",
|
||||
"category_id": product.category_id if product else None,
|
||||
"qty_sold": 0, "revenue": 0.0, "trackable_profit": 0.0,
|
||||
"uncosted_revenue": 0.0, "uncosted_items": 0, "order_ids": set(),
|
||||
}
|
||||
|
||||
result = []
|
||||
for entry in summary.values():
|
||||
pid = entry["product_id"]
|
||||
wdata = waste_by_product.get(pid, {"waste_qty": 0.0, "waste_cost": 0.0})
|
||||
entry["order_count"] = len(entry.pop("order_ids"))
|
||||
entry["revenue"] = round(entry["revenue"], 2)
|
||||
entry["trackable_profit"] = round(entry["trackable_profit"], 2)
|
||||
entry["uncosted_revenue"] = round(entry["uncosted_revenue"], 2)
|
||||
entry["has_gap"] = entry["uncosted_items"] > 0
|
||||
entry["waste_qty"] = round(wdata["waste_qty"], 3)
|
||||
entry["waste_cost"] = round(wdata["waste_cost"], 2)
|
||||
result.append(entry)
|
||||
|
||||
result.sort(key=lambda x: x["qty_sold"], reverse=True)
|
||||
|
||||
Reference in New Issue
Block a user