la-chariotte/la_chariotte/order/models.py

219 lines
7.2 KiB
Python

import random
import base36
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
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)
code = models.CharField(auto_created=True)
# Whether phone/email registration is possible/necessary for this grouped order
# 0 = disabled
# 1 = optional
# 2 = required
required_phone = models.IntegerField(
verbose_name="Numéro de téléphone",
default=2,
blank=True,
validators=[
MaxValueValidator(2),
MinValueValidator(0),
],
)
required_email = models.IntegerField(
verbose_name="Adresse email",
default=2,
blank=True,
validators=[
MaxValueValidator(2),
MinValueValidator(0),
],
)
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]
@property
def total_price(self):
price = 0
for order in self.order_set.all():
price += order.price
return price
def get_total_ordered_items(self):
total_nb = 0
for item in self.item_set.all():
total_nb += item.ordered_nb
return total_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)"""
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)
created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True)
note = models.TextField(max_length=200, null=True, blank=True)
@property
def 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
return articles_nb
@property
def price(self):
"""Computes the total price of the order"""
price = 0
for ord_item in self.ordered_items.all():
price += ord_item.get_price()
return price
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)
@property
def 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
return ordered_nb
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}"