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

163 lines
5.7 KiB
Python

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"))