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)