feat: Phase 2L — discount audit report
Backend:
- GET /api/reports/discounts: read-only audit of all OrderDiscount records
Filters: ?from=&to= or ?business_day_id=, ?applied_by=
Returns:
- discounts[]: id, order_id, table_name, applied_by_name, applied_at, discount_type,
discount_value, discount_amount (computed euro value), order_total_before,
item_id (null = whole-order), reason
- total_discount_value: sum of all euro discount amounts
- order_count: unique orders that received a discount
- by_waiter[]: grouped summary (waiter_name, count, total_value)
discount_amount computation: fixed → discount_value directly;
percent → computed from order or item total at query time
Frontend:
- DiscountsLog.jsx: read-only report under Reports → Λειτουργίες → Εκπτώσεις
- Filter bar: range/workday toggle, date range, waiter filter
- 3 stat cards: total discount value, orders with discounts, count of entries
- By-waiter summary table (hidden when only one waiter)
- Full audit table: date, order + table, waiter, type badge (% blue / € amber),
order total before, euro amount discounted in amber, reason
- Totals footer row
- No create/edit/delete — pure audit log
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1641,3 +1641,98 @@ def cancellations_export(
|
||||
} for c in data["cancellations"]]
|
||||
date_str = (from_dt or "")[:10]
|
||||
return _csv_response(rows, f"cancellations-{date_str}.csv")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2L — Discount audit report
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/discounts")
|
||||
def discounts_report(
|
||||
from_dt: Optional[str] = Query(default=None, alias="from"),
|
||||
to_dt: Optional[str] = Query(default=None, alias="to"),
|
||||
business_day_id: Optional[int] = None,
|
||||
applied_by: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
from models.order import OrderDiscount, OrderItem
|
||||
|
||||
q = db.query(OrderDiscount)
|
||||
if business_day_id:
|
||||
q = q.join(Order, Order.id == OrderDiscount.order_id).filter(Order.business_day_id == business_day_id)
|
||||
elif from_dt and to_dt:
|
||||
q = q.filter(
|
||||
OrderDiscount.applied_at >= datetime.fromisoformat(from_dt),
|
||||
OrderDiscount.applied_at <= datetime.fromisoformat(to_dt),
|
||||
)
|
||||
if applied_by:
|
||||
q = q.filter(OrderDiscount.applied_by == applied_by)
|
||||
|
||||
discounts = q.order_by(OrderDiscount.applied_at.desc()).all()
|
||||
|
||||
users_map = {u.id: u for u in db.query(User).all()}
|
||||
tables_map = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
||||
|
||||
# Preload orders for table names
|
||||
order_ids = {d.order_id for d in discounts}
|
||||
orders_map = {}
|
||||
if order_ids:
|
||||
for o in db.query(Order).filter(Order.id.in_(order_ids)).all():
|
||||
orders_map[o.id] = o
|
||||
|
||||
def _compute_discount_amount(d: OrderDiscount) -> float:
|
||||
"""Compute the actual euro amount discounted."""
|
||||
order = orders_map.get(d.order_id)
|
||||
if d.discount_type == "fixed":
|
||||
return round(d.discount_value, 2)
|
||||
if d.discount_type == "percent":
|
||||
if d.item_id:
|
||||
item = db.query(OrderItem).filter(OrderItem.id == d.item_id).first()
|
||||
if item:
|
||||
base = item.unit_price * item.quantity
|
||||
return round(base * d.discount_value / 100, 2)
|
||||
elif order:
|
||||
base = sum(i.unit_price * i.quantity for i in order.items if i.status != "cancelled")
|
||||
return round(base * d.discount_value / 100, 2)
|
||||
return 0.0
|
||||
|
||||
result = []
|
||||
total_discount_value = 0.0
|
||||
by_waiter: dict = {}
|
||||
|
||||
for d in discounts:
|
||||
order = orders_map.get(d.order_id)
|
||||
applier = users_map.get(d.applied_by)
|
||||
applier_name = (applier.full_name or applier.username) if applier else f"#{d.applied_by}"
|
||||
table_name = tables_map.get(order.table_id) if order and order.table_id else None
|
||||
order_total = sum(i.unit_price * i.quantity for i in order.items if i.status != "cancelled") if order else None
|
||||
discount_amount = _compute_discount_amount(d)
|
||||
total_discount_value += discount_amount
|
||||
|
||||
if d.applied_by not in by_waiter:
|
||||
by_waiter[d.applied_by] = {"waiter_id": d.applied_by, "waiter_name": applier_name, "count": 0, "total_value": 0.0}
|
||||
by_waiter[d.applied_by]["count"] += 1
|
||||
by_waiter[d.applied_by]["total_value"] = round(by_waiter[d.applied_by]["total_value"] + discount_amount, 2)
|
||||
|
||||
result.append({
|
||||
"id": d.id,
|
||||
"order_id": d.order_id,
|
||||
"table_name": table_name,
|
||||
"applied_by_id": d.applied_by,
|
||||
"applied_by_name": applier_name,
|
||||
"applied_at": _dt(d.applied_at),
|
||||
"discount_type": d.discount_type,
|
||||
"discount_value": d.discount_value,
|
||||
"discount_amount": discount_amount,
|
||||
"order_total_before": round(order_total, 2) if order_total is not None else None,
|
||||
"item_id": d.item_id,
|
||||
"reason": d.reason,
|
||||
})
|
||||
|
||||
return {
|
||||
"discounts": result,
|
||||
"total_discount_value": round(total_discount_value, 2),
|
||||
"order_count": len({d["order_id"] for d in result}),
|
||||
"by_waiter": sorted(by_waiter.values(), key=lambda x: -x["total_value"]),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user