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
|
2023-03-27 16:45:00 +02:00
|
|
|
from django.db.models import Avg, Count, F, Sum
|
2022-04-25 23:11:37 +02:00
|
|
|
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-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
|
|
|
|
|
|
|
|
|
2022-04-25 18:37:26 +02:00
|
|
|
class PaymentMethodQuerySet(models.QuerySet):
|
|
|
|
def with_turnover(self):
|
|
|
|
return self.annotate(
|
2022-04-25 23:11:37 +02:00
|
|
|
turnover=Coalesce(
|
|
|
|
Sum(
|
|
|
|
F("baskets__items__quantity")
|
2023-03-25 20:01:14 +01:00
|
|
|
* F("baskets__items__unit_price_cents"),
|
2022-04-25 23:11:37 +02:00
|
|
|
),
|
|
|
|
0,
|
2023-03-25 20:01:14 +01:00
|
|
|
),
|
2022-04-25 18:37:26 +02:00
|
|
|
)
|
|
|
|
|
2022-04-25 18:59:32 +02:00
|
|
|
def with_sold(self):
|
|
|
|
return self.annotate(sold=Count("baskets", distinct=True))
|
|
|
|
|
2022-04-25 18:37:26 +02:00
|
|
|
|
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 18:37:26 +02:00
|
|
|
|
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():
|
2022-04-26 18:57:30 +02:00
|
|
|
last = Product.objects.last()
|
|
|
|
if last is None:
|
|
|
|
return 1
|
|
|
|
return last.display_order + 1
|
2022-04-24 16:21:39 +02:00
|
|
|
|
|
|
|
|
2022-04-25 18:37:26 +02:00
|
|
|
class ProductQuerySet(models.QuerySet):
|
|
|
|
def with_turnover(self):
|
|
|
|
return self.annotate(
|
2022-04-25 23:11:37 +02:00
|
|
|
turnover=Coalesce(
|
2022-04-27 20:46:02 +02:00
|
|
|
Sum(F("basket_items__quantity") * F("basket_items__unit_price_cents")),
|
|
|
|
0,
|
2023-03-25 20:01:14 +01:00
|
|
|
),
|
2022-04-25 18:37:26 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
def with_sold(self):
|
2022-04-25 23:11:37 +02:00
|
|
|
return self.annotate(sold=Coalesce(Sum("basket_items__quantity"), 0))
|
2022-04-25 18:37:26 +02:00
|
|
|
|
2023-03-27 16:45:00 +02:00
|
|
|
def with_fixed_price(self):
|
|
|
|
return self.exclude(unit_price_cents=0)
|
|
|
|
|
|
|
|
def with_no_fixed_price(self):
|
|
|
|
return self.filter(unit_price_cents=0)
|
|
|
|
|
2022-04-25 18:37:26 +02:00
|
|
|
|
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"))
|
|
|
|
unit_price_cents = models.PositiveIntegerField(
|
2023-03-25 20:01:14 +01:00
|
|
|
verbose_name=_("unit price (cents)"),
|
2023-03-27 16:45:00 +02:00
|
|
|
help_text=_(
|
|
|
|
"Unit price in cents. Use zero to denote that the product has no fixed price.",
|
|
|
|
),
|
2022-04-25 23:04:49 +02:00
|
|
|
)
|
2023-04-02 15:51:54 +02:00
|
|
|
initials = models.CharField(
|
|
|
|
max_length=10,
|
|
|
|
verbose_name=_("initials"),
|
|
|
|
blank=False,
|
|
|
|
null=False,
|
|
|
|
)
|
2022-04-25 23:04:49 +02:00
|
|
|
display_order = models.PositiveIntegerField(
|
2023-03-25 20:01:14 +01:00
|
|
|
default=default_product_display_order,
|
|
|
|
verbose_name=_("display order"),
|
2022-04-25 23:04:49 +02:00
|
|
|
)
|
2022-04-24 16:21:39 +02:00
|
|
|
|
2022-04-26 22:56:03 +02:00
|
|
|
objects = ProductManager.from_queryset(ProductQuerySet)()
|
2022-04-25 18:37:26 +02:00
|
|
|
|
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(
|
2023-03-25 20:01:14 +01:00
|
|
|
hashlib.sha256(bytes(self.name, encoding="utf-8")).hexdigest()[:2],
|
|
|
|
base=16,
|
2022-04-28 18:34:13 +02:00
|
|
|
)
|
|
|
|
|
2023-03-27 16:45:00 +02:00
|
|
|
@property
|
|
|
|
def has_fixed_price(self) -> bool:
|
|
|
|
return self.unit_price_cents > 0
|
|
|
|
|
2022-04-24 16:21:39 +02:00
|
|
|
|
2022-04-25 18:37:26 +02:00
|
|
|
class BasketQuerySet(models.QuerySet):
|
2022-04-26 18:19:53 +02:00
|
|
|
def priced(self) -> BasketQuerySet:
|
2022-04-25 18:37:26 +02:00
|
|
|
return self.annotate(
|
2023-03-25 20:01:14 +01:00
|
|
|
price=Coalesce(Sum(F("items__quantity") * F("items__unit_price_cents")), 0),
|
2022-04-25 18:37:26 +02:00
|
|
|
)
|
|
|
|
|
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"]
|
|
|
|
|
2022-04-25 18:37:26 +02:00
|
|
|
|
2022-04-24 16:21:39 +02:00
|
|
|
class Basket(Model):
|
|
|
|
payment_method = models.ForeignKey(
|
|
|
|
to=PaymentMethod,
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
related_name="baskets",
|
2023-03-27 18:26:40 +02:00
|
|
|
null=False,
|
|
|
|
blank=False,
|
2022-04-25 23:04:49 +02:00
|
|
|
verbose_name=_("payment method"),
|
2022-04-24 16:21:39 +02:00
|
|
|
)
|
|
|
|
|
2022-04-25 18:37:26 +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
|
|
|
|
2022-04-25 18:37:26 +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-25 18:37:26 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
2022-04-25 18:37:26 +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-09-25 21:08:44 +02:00
|
|
|
|
|
|
|
|
2022-09-25 21:46:14 +02:00
|
|
|
class Cache(SingletonModel):
|
|
|
|
etag = models.UUIDField(default=uuid.uuid4)
|
|
|
|
last_modified = models.DateTimeField(auto_now=True)
|
2022-09-25 21:08:44 +02:00
|
|
|
|
|
|
|
def __str__(self) -> str:
|
2022-09-25 21:46:14 +02:00
|
|
|
return str(self.etag)
|
2022-09-25 21:08:44 +02:00
|
|
|
|
|
|
|
def refresh(self):
|
2022-09-25 21:46:14 +02:00
|
|
|
self.etag = uuid.uuid4()
|
2022-09-25 21:08:44 +02:00
|
|
|
self.save()
|
2022-09-25 21:46:14 +02:00
|
|
|
|
|
|
|
|
2023-03-25 20:01:14 +01:00
|
|
|
def reports_etag(_request):
|
2022-09-25 21:46:14 +02:00
|
|
|
return str(Cache.get_solo().etag)
|
|
|
|
|
|
|
|
|
2023-03-25 20:01:14 +01:00
|
|
|
def reports_last_modified(_request):
|
2022-09-25 21:46:14 +02:00
|
|
|
return Cache.get_solo().last_modified
|