39818-vm/core/views.py
2026-04-26 16:42:14 +00:00

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)