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:
2026-06-08 03:43:19 +03:00
parent 184bee5942
commit 492096007b
3 changed files with 288 additions and 0 deletions

View File

@@ -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"]),
}