checkout/src/purchase/models.py

236 lines
6.5 KiB
Python
Raw Normal View History

2022-04-26 18:19:53 +02:00
from __future__ import annotations
2022-04-28 18:34:13 +02:00
import hashlib
2022-09-25 21:08:44 +02:00
import uuid
2022-04-28 18:34:13 +02:00
2022-04-24 16:21:39 +02:00
from django.db import models
2022-05-05 19:11:30 +02:00
from django.db.models import Avg, Count, F, Sum, UniqueConstraint
from django.db.models.functions import Coalesce
2022-04-24 18:59:04 +02:00
from django.urls import reverse
2022-04-25 23:04:49 +02:00
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
2022-04-24 19:19:01 +02:00
from PIL import Image, ImageOps
2022-09-25 21:08:44 +02:00
from solo.models import SingletonModel
2022-04-24 16:21:39 +02:00
class Model(models.Model):
2022-04-25 23:04:49 +02:00
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("updated at"))
2022-04-24 16:21:39 +02:00
class Meta:
abstract = True
class PaymentMethodQuerySet(models.QuerySet):
def with_turnover(self):
return self.annotate(
turnover=Coalesce(
Sum(
F("baskets__items__quantity")
2022-04-27 20:46:02 +02:00
* F("baskets__items__unit_price_cents")
),
0,
)
)
2022-04-25 18:59:32 +02:00
def with_sold(self):
return self.annotate(sold=Count("baskets", distinct=True))
2022-04-26 22:41:26 +02:00
class PaymentMethodManager(models.Manager):
def get_by_natural_key(self, name):
return self.get(name=name)
2022-04-24 16:21:39 +02:00
class PaymentMethod(Model):
2022-04-25 23:04:49 +02:00
name = models.CharField(max_length=50, unique=True, verbose_name=_("name"))
2022-04-24 16:21:39 +02:00
2022-04-26 22:56:03 +02:00
objects = PaymentMethodManager.from_queryset(PaymentMethodQuerySet)()
2022-04-25 23:04:49 +02:00
class Meta:
verbose_name = _("payment method")
verbose_name_plural = _("payment methods")
2022-04-24 16:21:39 +02:00
def __str__(self):
return self.name
2022-04-26 22:41:26 +02:00
def natural_key(self):
return (self.name,)
2022-04-24 16:21:39 +02:00
def default_product_display_order():
last = Product.objects.last()
if last is None:
return 1
return last.display_order + 1
2022-04-24 16:21:39 +02:00
class ProductQuerySet(models.QuerySet):
def with_turnover(self):
return self.annotate(
turnover=Coalesce(
2022-04-27 20:46:02 +02:00
Sum(F("basket_items__quantity") * F("basket_items__unit_price_cents")),
0,
)
)
def with_sold(self):
return self.annotate(sold=Coalesce(Sum("basket_items__quantity"), 0))
2022-04-26 22:41:26 +02:00
class ProductManager(models.Manager):
def get_by_natural_key(self, name):
return self.get(name=name)
2022-04-24 16:21:39 +02:00
class Product(Model):
2022-04-25 23:04:49 +02:00
name = models.CharField(max_length=250, unique=True, verbose_name=_("name"))
image = models.ImageField(null=True, blank=True, verbose_name=_("image"))
unit_price_cents = models.PositiveIntegerField(
verbose_name=_("unit price (cents)"), help_text=_("unit price in cents")
)
display_order = models.PositiveIntegerField(
default=default_product_display_order, verbose_name=_("display order")
)
2022-04-24 16:21:39 +02:00
2022-04-26 22:56:03 +02:00
objects = ProductManager.from_queryset(ProductQuerySet)()
2022-04-24 16:21:39 +02:00
class Meta:
ordering = ["display_order", "name"]
2022-04-25 23:04:49 +02:00
verbose_name = _("product")
verbose_name_plural = _("products")
2022-04-24 16:21:39 +02:00
def __str__(self):
return self.name
2022-04-26 22:41:26 +02:00
def natural_key(self):
return (self.name,)
2022-04-28 18:34:13 +02:00
@property
def color_hue(self):
return int(
hashlib.sha256(bytes(self.name, encoding="utf-8")).hexdigest()[:2], base=16
)
2022-04-24 19:19:01 +02:00
def save(self, *args, **kwargs):
super().save()
2022-04-25 23:11:43 +02:00
if not self.image:
return
2022-04-24 19:19:01 +02:00
with Image.open(self.image.path) as img:
img = ImageOps.exif_transpose(img)
width, height = img.size # Get dimensions
if width > 300 and height > 300:
# keep ratio but shrink down
img.thumbnail((width, height))
# check which one is smaller
if height < width:
# make square by cutting off equal amounts left and right
left = (width - height) / 2
right = (width + height) / 2
top = 0
bottom = height
img = img.crop((left, top, right, bottom))
elif width < height:
# make square by cutting off bottom
left = 0
right = width
top = 0
bottom = width
img = img.crop((left, top, right, bottom))
if width > 300 and height > 300:
img.thumbnail((300, 300))
img.save(self.image.path)
2022-04-24 16:21:39 +02:00
class BasketQuerySet(models.QuerySet):
2022-04-26 18:19:53 +02:00
def priced(self) -> BasketQuerySet:
return self.annotate(
2022-04-27 20:46:02 +02:00
price=Coalesce(Sum(F("items__quantity") * F("items__unit_price_cents")), 0)
)
2022-04-26 18:19:53 +02:00
def average_basket(self) -> float:
return self.priced().aggregate(avg=Avg("price"))["avg"]
def by_date(self, date) -> BasketQuerySet:
return self.filter(created_at__date=date)
def turnover(self) -> int:
return self.priced().aggregate(total=Sum("price"))["total"]
def no_payment_method(self) -> BasketQuerySet:
2022-04-25 18:59:32 +02:00
return self.filter(payment_method=None)
2022-04-24 16:21:39 +02:00
class Basket(Model):
payment_method = models.ForeignKey(
to=PaymentMethod,
on_delete=models.PROTECT,
related_name="baskets",
null=True,
blank=True,
2022-04-25 23:04:49 +02:00
verbose_name=_("payment method"),
2022-04-24 16:21:39 +02:00
)
objects = BasketQuerySet.as_manager()
2022-04-25 23:04:49 +02:00
class Meta:
verbose_name = _("basket")
verbose_name_plural = _("baskets")
2022-04-24 16:21:39 +02:00
def __str__(self):
2022-04-25 23:04:49 +02:00
return gettext("Basket #%(id)s") % {"id": self.id}
2022-04-24 16:21:39 +02:00
2022-04-24 18:59:04 +02:00
def get_absolute_url(self):
return reverse("purchase:update", args=(self.pk,))
2022-04-24 16:21:39 +02:00
class BasketItemQuerySet(models.QuerySet):
def priced(self):
2022-04-27 20:46:02 +02:00
return self.annotate(price=Coalesce(F("quantity") * F("unit_price_cents"), 0))
2022-04-24 16:21:39 +02:00
class BasketItem(Model):
product = models.ForeignKey(
2022-04-25 23:04:49 +02:00
to=Product,
on_delete=models.PROTECT,
related_name="basket_items",
verbose_name=_("product"),
2022-04-24 16:21:39 +02:00
)
basket = models.ForeignKey(
2022-04-25 23:04:49 +02:00
to=Basket,
on_delete=models.CASCADE,
related_name="items",
verbose_name=_("basket"),
2022-04-24 16:21:39 +02:00
)
2022-04-25 23:04:49 +02:00
quantity = models.PositiveIntegerField(verbose_name=_("quantity"))
2022-04-27 20:46:02 +02:00
unit_price_cents = models.PositiveIntegerField(
verbose_name=_("unit price (cents)"),
help_text=_("product's unit price in cents at the time of purchase"),
)
2022-04-24 16:21:39 +02:00
objects = BasketItemQuerySet.as_manager()
2022-04-25 23:04:49 +02:00
class Meta:
verbose_name = _("basket item")
verbose_name_plural = _("basket items")
2022-05-05 19:11:30 +02:00
constraints = [
UniqueConstraint("product", "basket", name="unique_product_per_basket")
]
2022-09-25 21:08:44 +02:00
class CacheEtag(SingletonModel):
value = models.UUIDField(default=uuid.uuid4)
def __str__(self) -> str:
return str(self.value)
def refresh(self):
self.value = uuid.uuid4()
self.save()