la-chariotte/la_chariotte/order/models.py

254 lines
8.4 KiB
Python

import logging
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
logger = logging.getLogger(__name__)
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")
# Associate with zero/more saved distribution places
places = models.ManyToManyField(
"order.Place", verbose_name="Lieux de distribution", related_name="orders"
)
description = models.TextField("Description", null=True, blank=True)
code = models.CharField(auto_created=True)
is_phone_mandatory = models.BooleanField(
default=False, verbose_name="Numéro de téléphone obligatoire"
)
class Meta:
indexes = [models.Index(fields=["code"])]
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)
place = models.ForeignKey(
"order.place", on_delete=models.CASCADE, 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}"
class Place(models.Model):
name = models.CharField(max_length=100, verbose_name="Nom du lieu de distribution")
orga = models.ForeignKey(
AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="Organisateur·ice"
)
code = models.CharField(
max_length=20,
verbose_name="Identifiant unique du lieu (raccourci)",
unique=True,
)
description = models.TextField("Description", null=True, blank=True)
class Meta:
indexes = [models.Index(fields=["code"])]
def __str__(self): # pragma: no cover
return self.name
def get_absolute_url(self):
return reverse("order:place_overview", kwargs={"code": self.code})
def active_orders(self):
# Only return currently active orders (not yet delivered)
active_orders = (
self.orders.all()
.filter(delivery_date__gt=timezone.now())
.order_by("-delivery_date")
)
return active_orders
def save(self, *args, **kwargs):
# Check that self.code was not changed
if self.pk is not None:
original = Place.objects.get(pk=self.pk)
if original.code != self.code:
# Restore the old place code
self.code = original.code
logger.warn(
"Le code du lieu '%s' ne peut pas changer après la création"
% self.name
)
super().save()