163 lines
6.2 KiB
Python
163 lines
6.2 KiB
Python
from functools import wraps
|
|
from urllib.parse import quote
|
|
|
|
from django.contrib import messages
|
|
from django.db import transaction
|
|
from django.db.models import DecimalField, ExpressionWrapper, F, Prefetch, Sum
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
|
|
from .forms import AddOrderItemForm, OrderStatusForm
|
|
from .models import Order, Product, Table, UserRole
|
|
|
|
|
|
LINE_TOTAL = ExpressionWrapper(
|
|
F("items__quantity") * F("items__product__price"),
|
|
output_field=DecimalField(max_digits=10, decimal_places=2),
|
|
)
|
|
|
|
|
|
def role_required(*allowed_roles):
|
|
def decorator(view_func):
|
|
@wraps(view_func)
|
|
def wrapped(request, *args, **kwargs):
|
|
if not request.user.is_authenticated:
|
|
return redirect(f"/login/?next={quote(request.get_full_path())}")
|
|
|
|
resolved_role = UserRole.resolve_for(request.user)
|
|
if allowed_roles and resolved_role not in allowed_roles:
|
|
messages.error(request, "Tu rol no tiene acceso a esta sección.")
|
|
return redirect("home")
|
|
return view_func(request, *args, **kwargs)
|
|
|
|
return wrapped
|
|
|
|
return decorator
|
|
|
|
|
|
def _dashboard_context():
|
|
active_orders = Order.objects.exclude(status=Order.Status.PAID).prefetch_related("items__product").order_by("-created_at")
|
|
tables = Table.objects.prefetch_related(
|
|
Prefetch("orders", queryset=active_orders, to_attr="open_orders_cache")
|
|
)
|
|
recent_orders = Order.objects.select_related("table").prefetch_related("items__product")[:6]
|
|
kitchen_orders = (
|
|
Order.objects.filter(status__in=[Order.Status.OPEN, Order.Status.PREPARING])
|
|
.select_related("table")
|
|
.prefetch_related("items__product")[:5]
|
|
)
|
|
daily_revenue = (
|
|
Order.objects.filter(status=Order.Status.PAID, paid_at__date=timezone.localdate())
|
|
.aggregate(total=Sum(LINE_TOTAL))["total"]
|
|
or 0
|
|
)
|
|
top_products = (
|
|
Product.objects.filter(order_items__order__status=Order.Status.PAID)
|
|
.annotate(sold=Sum("order_items__quantity"))
|
|
.order_by("-sold", "name")[:4]
|
|
)
|
|
|
|
return {
|
|
"tables": tables,
|
|
"recent_orders": recent_orders,
|
|
"kitchen_orders": kitchen_orders,
|
|
"available_products": Product.objects.filter(is_available=True).count(),
|
|
"occupied_tables": Table.objects.filter(status=Table.Status.OCCUPIED).count(),
|
|
"daily_revenue": daily_revenue,
|
|
"top_products": top_products,
|
|
"meta_title": "Restaurante POS | Operación de mesas, cocina y cobro",
|
|
"meta_description": "Dashboard táctil para controlar mesas, comandas, cocina y cobro desde una sola interfaz ligera.",
|
|
}
|
|
|
|
|
|
@role_required(UserRole.Role.ADMIN, UserRole.Role.WAITER, UserRole.Role.KITCHEN)
|
|
def home(request):
|
|
context = _dashboard_context()
|
|
context["resolved_user_role"] = UserRole.label_for(request.user)
|
|
return render(request, "core/index.html", context)
|
|
|
|
|
|
@role_required(UserRole.Role.ADMIN, UserRole.Role.WAITER)
|
|
def table_detail(request, table_id):
|
|
table = get_object_or_404(
|
|
Table.objects.prefetch_related(
|
|
Prefetch(
|
|
"orders",
|
|
queryset=Order.objects.exclude(status=Order.Status.PAID).prefetch_related("items__product").order_by("-created_at"),
|
|
to_attr="open_orders_cache",
|
|
)
|
|
),
|
|
pk=table_id,
|
|
)
|
|
current_order = table.current_order
|
|
|
|
if request.method == "POST":
|
|
form = AddOrderItemForm(request.POST)
|
|
if form.is_valid():
|
|
with transaction.atomic():
|
|
if current_order is None:
|
|
current_order = Order.objects.create(table=table)
|
|
item = form.save(commit=False)
|
|
item.order = current_order
|
|
item.save()
|
|
if table.status != Table.Status.OCCUPIED:
|
|
table.status = Table.Status.OCCUPIED
|
|
table.save(update_fields=["status", "updated_at"])
|
|
messages.success(request, f"Se agregó {item.product.name} a {table.name}.")
|
|
return redirect("table_detail", table_id=table.pk)
|
|
else:
|
|
form = AddOrderItemForm()
|
|
|
|
context = {
|
|
"table": table,
|
|
"current_order": current_order,
|
|
"form": form,
|
|
"meta_title": f"{table.name} | Comanda activa",
|
|
"meta_description": f"Captura de comandas y notas para {table.name}.",
|
|
"resolved_user_role": UserRole.label_for(request.user),
|
|
}
|
|
return render(request, "core/table_detail.html", context)
|
|
|
|
|
|
@role_required(UserRole.Role.ADMIN, UserRole.Role.WAITER, UserRole.Role.KITCHEN)
|
|
def order_detail(request, order_id):
|
|
order = get_object_or_404(Order.objects.select_related("table").prefetch_related("items__product"), pk=order_id)
|
|
|
|
if request.method == "POST":
|
|
form = OrderStatusForm(request.POST, order=order)
|
|
if form.is_valid():
|
|
next_status = form.cleaned_data["status"]
|
|
with transaction.atomic():
|
|
order.advance_to(next_status)
|
|
messages.success(request, f"La orden #{order.pk} ahora está {order.get_status_display().lower()}.")
|
|
return redirect("order_detail", order_id=order.pk)
|
|
status_forms = {
|
|
status: OrderStatusForm(order=order, initial={"status": status})
|
|
for status in order.allowed_transitions()
|
|
}
|
|
|
|
context = {
|
|
"order": order,
|
|
"status_forms": status_forms,
|
|
"meta_title": f"Orden #{order.pk} | {order.table.name}",
|
|
"meta_description": f"Detalle de la orden #{order.pk} con total, ítems y estado operativo.",
|
|
"resolved_user_role": UserRole.label_for(request.user),
|
|
}
|
|
return render(request, "core/order_detail.html", context)
|
|
|
|
|
|
@role_required(UserRole.Role.ADMIN, UserRole.Role.KITCHEN)
|
|
def kitchen_board(request):
|
|
orders = (
|
|
Order.objects.filter(status__in=[Order.Status.OPEN, Order.Status.PREPARING])
|
|
.select_related("table")
|
|
.prefetch_related("items__product")
|
|
)
|
|
context = {
|
|
"orders": orders,
|
|
"meta_title": "KDS | Cocina pendiente",
|
|
"meta_description": "Pantalla de cocina con órdenes pendientes de preparación.",
|
|
"resolved_user_role": UserRole.label_for(request.user),
|
|
}
|
|
return render(request, "core/kitchen.html", context)
|