import random import base36 from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils import timezone from la_chariotte.settings import AUTH_USER_MODEL class GroupedOrder(models.Model): name = models.CharField( max_length=100, null=True, verbose_name="Titre de la commande" ) orga = models.ForeignKey( AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="Organisateur·ice" ) delivery_date = models.DateField("Date de livraison") delivery_slot = models.CharField( max_length=50, null=True, blank=True, verbose_name="Créneau de distribution" ) deadline = models.DateTimeField("Date limite de commande") place = models.CharField( max_length=100, null=True, blank=True, verbose_name="Lieu de livraison" ) description = models.TextField("Description", null=True, blank=True) total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) code = models.CharField(auto_created=True) def create_code_from_pk(self): """When a grouped order is created, a unique code is generated, to be used to build the order URL. Using a simple code (rather than a UUID) makes it easy to be manually written. This code is generated using : 1. The record private key, written in base36 (max 5 digits) 2. A random int written in base36 (5 digits long) Only the 6 first digits of this string are used. The use of the private key guarantees the uniqueness, and the random part makes the URL path hard to guess. """ code_length = 6 base_36_pk = base36.dumps(self.pk) # The random int is between 11111[in base 36] and zzzzz[in base 36], # which are the smallest and the biggest values with 5 digits. random_string = base36.dumps( random.randint(pow(36, code_length - 2), pow(36, code_length - 1) - 1) ) self.code = f"{base_36_pk}{random_string}"[:code_length] def compute_total_price(self): price = 0 for order in self.order_set.all(): price += order.price self.total_price = price self.save() def compute_items_ordered_nb(self): for item in self.item_set.all(): item.compute_ordered_nb() def is_open(self): return self.deadline >= timezone.now() def is_to_be_delivered(self): return self.delivery_date >= timezone.now().date() def get_absolute_url(self): return reverse("order:manage_items", kwargs={"code": self.code}) def clean_fields(self, exclude=None): super().clean_fields(exclude=exclude) # Ensure that new grouped orders use a delivery_date in the future. # (updating an existing one with a date in the past is still possible) if self.delivery_date < timezone.now().date() and not self.pk: raise ValidationError("La date de livraison ne doit pas être dans le passé") if self.delivery_date < self.deadline.date(): raise ValidationError( "La date limite de commande doit être avant la date de livraison" ) def __str__(self): # pragma: no cover return ( self.name if self.name else ( f"Commande groupée {self.code} du {self.date} organisée par {self.orga}" ) ) class OrderAuthor(models.Model): """Used when a user orders (with or without an account)""" # TODO faire le lien avec CustomUser (CustomUser hérite de OrderAuthor), # pour ensuite préremplir quand on est connecté·e first_name = models.CharField(verbose_name="Prénom") last_name = models.CharField(verbose_name="Nom") phone = models.CharField( verbose_name="Numéro de téléphone", help_text="Pour que l'organisateur·ice vous contacte en cas de besoin", ) email = models.CharField( verbose_name="Adresse mail", help_text="Pour que l'organisateur·ice vous contacte en cas de besoin", ) def __str__(self): # pragma: no cover return f"{self.first_name} {self.last_name}" class Order(models.Model): grouped_order = models.ForeignKey( GroupedOrder, on_delete=models.CASCADE, related_name="order_set" ) author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE) articles_nb = models.PositiveIntegerField(default=0) price = models.DecimalField(max_digits=10, decimal_places=2, default=0) created_date = models.DateTimeField("Date de la commande", auto_now_add=True) note = models.TextField(max_length=200, null=True, blank=True) def compute_order_articles_nb(self): """Computes the number of articles in this order""" articles_nb = 0 for ord_item in self.ordered_items.all(): articles_nb += ord_item.nb self.articles_nb = articles_nb self.save() def compute_order_price(self): """Computes the total price of the order""" price = 0 for ord_item in self.ordered_items.all(): price += ord_item.get_price() self.price = price self.save() def __str__(self): # pragma: no cover return ( f"Commande de {self.author} pour la commande groupée" f" {self.grouped_order.code}" ) class Item(models.Model): name = models.CharField(max_length=100) grouped_order = models.ForeignKey(GroupedOrder, on_delete=models.CASCADE) price = models.DecimalField(max_digits=10, decimal_places=2) max_limit = models.PositiveSmallIntegerField(null=True, blank=True) ordered_nb = models.IntegerField(default=0) def compute_ordered_nb(self): """Computes the number of times this item has been ordered""" ordered_nb = 0 for order in self.orders.all(): ordered_nb += order.nb self.ordered_nb = ordered_nb self.save() def get_total_price(self): """Returns the total price of all orders on this item""" return self.price * self.ordered_nb def get_remaining_nb(self): """Returns the number of remaining articles for this item""" if self.max_limit is not None: return self.max_limit - self.ordered_nb else: return None def get_absolute_url(self): return reverse("order:manage_items", kwargs={"code": self.grouped_order.code}) def __str__(self): # pragma: no cover return f"{self.name} ({self.price} €)" class Meta: ordering = ["name"] class OrderedItem(models.Model): """Item in one specific Order, and the number of articles""" nb = models.PositiveSmallIntegerField(default=0) order = models.ForeignKey( Order, on_delete=models.CASCADE, related_name="ordered_items" ) item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name="orders") def get_price(self): return self.nb * self.item.price def __str__(self): # pragma: no cover return f"{self.nb} {self.item}, dans la commande {self.order.pk}"