from decimal import Decimal from django.conf import settings from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone class UserRole(models.Model): class Role(models.TextChoices): ADMIN = "admin", "Admin" WAITER = "waiter", "Mesero" KITCHEN = "kitchen", "Cocina" user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="role_profile") role = models.CharField(max_length=20, choices=Role.choices, default=Role.WAITER) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["user__username"] verbose_name = "Rol de usuario" verbose_name_plural = "Roles de usuario" def __str__(self): return f"{self.user.username} · {self.get_role_display()}" @classmethod def resolve_for(cls, user): if not user or not user.is_authenticated: return None if user.is_superuser: return cls.Role.ADMIN profile, _ = cls.objects.get_or_create(user=user, defaults={"role": cls.Role.WAITER}) return profile.role @classmethod def label_for(cls, user): role = cls.resolve_for(user) return dict(cls.Role.choices).get(role, "Sin rol") if role else "Invitado" @receiver(post_save, sender=settings.AUTH_USER_MODEL) def ensure_user_role(sender, instance, created, **kwargs): if created and not instance.is_superuser: UserRole.objects.get_or_create(user=instance, defaults={"role": UserRole.Role.WAITER}) class Table(models.Model): class Status(models.TextChoices): FREE = "free", "Libre" OCCUPIED = "occupied", "Ocupada" name = models.CharField(max_length=40, unique=True) seats = models.PositiveSmallIntegerField(default=4) status = models.CharField(max_length=12, choices=Status.choices, default=Status.FREE) area = models.CharField(max_length=50, blank=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["name"] def __str__(self): return self.name @property def current_order(self): cached_orders = getattr(self, "open_orders_cache", None) if cached_orders is not None: return cached_orders[0] if cached_orders else None return self.orders.exclude(status=Order.Status.PAID).order_by("-created_at").first() class Product(models.Model): name = models.CharField(max_length=120) category = models.CharField(max_length=80) price = models.DecimalField(max_digits=10, decimal_places=2) is_available = models.BooleanField(default=True) station = models.CharField(max_length=80, default="Cocina") class Meta: ordering = ["category", "name"] def __str__(self): return self.name class Order(models.Model): class Status(models.TextChoices): OPEN = "open", "Abierta" PREPARING = "preparing", "En preparación" READY = "ready", "Lista" PAID = "paid", "Pagada" table = models.ForeignKey(Table, on_delete=models.PROTECT, related_name="orders") status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN) guest_name = models.CharField(max_length=120, blank=True) server_note = models.CharField(max_length=200, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) sent_to_kitchen_at = models.DateTimeField(null=True, blank=True) paid_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ["-created_at"] def __str__(self): return f"Orden #{self.pk} · {self.table.name}" @property def subtotal(self): if hasattr(self, "subtotal_value") and self.subtotal_value is not None: return self.subtotal_value total = sum((item.line_total for item in self.items.select_related("product").all()), Decimal("0.00")) return total.quantize(Decimal("0.01")) @property def total_items(self): if hasattr(self, "items_count") and self.items_count is not None: return self.items_count return sum(item.quantity for item in self.items.all()) def allowed_transitions(self): transitions = { self.Status.OPEN: [self.Status.PREPARING], self.Status.PREPARING: [self.Status.READY], self.Status.READY: [self.Status.PAID], self.Status.PAID: [], } return transitions.get(self.status, []) def advance_to(self, new_status): if new_status not in self.allowed_transitions(): raise ValueError("Invalid status transition") now = timezone.now() self.status = new_status if new_status == self.Status.PREPARING and not self.sent_to_kitchen_at: self.sent_to_kitchen_at = now if new_status == self.Status.PAID: self.paid_at = now self.table.status = Table.Status.FREE self.table.save(update_fields=["status", "updated_at"]) self.save(update_fields=["status", "sent_to_kitchen_at", "paid_at", "updated_at"]) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name="order_items") quantity = models.PositiveIntegerField(default=1) note = models.CharField(max_length=200, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created_at", "id"] def __str__(self): return f"{self.quantity} x {self.product.name}" @property def line_total(self): return (self.product.price * self.quantity).quantize(Decimal("0.01"))