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 CODE_LENGTH = 6 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") 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, we compute a unique code that will be used in url path How we generate this code : 1. The instance pk, written in base36 (max 5 digits for now - we assume that there will not be more than 60466175 grouped orders) 2. A random int written in base36 (5 digits long) 3. Only the 6 first digits of this string The use of pk in the beginning of the string guarantees the uniqueness, and the random part makes that we cannot guess the url path. We use this kind of code, that is small and can contain a large number of values, in order to keep it simple enough to be manually written, so that people can share information about grouped orders more easily """ base_36_pk = base36.dumps(self.pk) # generate a 5 digits long string : 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): """Updates the ordered_nb of all items fo the grouped_order""" for item in self.item_set.all(): item.compute_ordered_nb() def is_ongoing(self): """Returns True if the grouped order is open for new Orders - False if it's too late""" return self.deadline >= timezone.now() def is_to_be_delivered(self): """Returns True if the grouped order has not been delivered yet - False if it's old""" 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) if self.delivery_date < timezone.now().date() and not self.pk: # if the grouped order is being created (not updated), it cannot be in the past # if we are updating, it's ok 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): """Created when a user orders without having an account - or when a user creates 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 {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 ordered articles for this item (in this grouped order)""" 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 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 in this grouped order""" 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) # works up to 32767 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}"