163 lines
5.7 KiB
Python
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"))
|