diff --git a/src/shop/admin.py b/src/shop/admin.py
index 426ec96b..b4550f4c 100644
--- a/src/shop/admin.py
+++ b/src/shop/admin.py
@@ -90,6 +90,15 @@ def available_to(product):
available_to.short_description = 'Available to'
+def stock_info(product):
+ if product.stock_amount:
+ return "{} / {}".format(
+ product.left_in_stock,
+ product.stock_amount
+ )
+ return "N/A"
+
+
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = [
@@ -98,6 +107,7 @@ class ProductAdmin(admin.ModelAdmin):
'ticket_type',
'price',
'description',
+ stock_info,
available_from,
available_to
]
diff --git a/src/shop/models.py b/src/shop/models.py
index a3ab1329..adf97027 100644
--- a/src/shop/models.py
+++ b/src/shop/models.py
@@ -349,10 +349,16 @@ class Product(CreatedUpdatedModel, UUIDModel):
- Whether now is in the self.available_in
- If a stock is defined, that there are items left
"""
+ predicates = [self.is_time_available]
+ if self.stock_amount:
+ predicates.append(self.is_stock_available)
+ return all(predicates)
+
+ @property
+ def is_time_available(self):
now = timezone.now()
time_available = now in self.available_in
- stock_available = self.left_in_stock() > 0
- return time_available and stock_available
+ return time_available
def is_old(self):
now = timezone.now()
@@ -364,16 +370,26 @@ class Product(CreatedUpdatedModel, UUIDModel):
now = timezone.now()
return self.available_in.lower > now
+ @property
def left_in_stock(self):
- sold = OrderProductRelation.objects.filter(
- product=self,
- order__paid=True,
- ).aggregate(Sum('quantity'))['quantity__sum']
+ if self.stock_amount:
+ sold = OrderProductRelation.objects.filter(
+ product=self,
+ order__paid=True,
+ ).aggregate(Sum('quantity'))['quantity__sum']
- total_left = self.stock_amount - (sold or 0)
+ total_left = self.stock_amount - (sold or 0)
- return total_left
+ return total_left
+ return None
+ @property
+ def is_stock_available(self):
+ if self.stock_amount:
+ stock_available = self.left_in_stock > 0
+ return stock_available
+ # If there is no stock defined the product is generally available.
+ return True
class OrderProductRelation(CreatedUpdatedModel):
order = models.ForeignKey('shop.Order', on_delete=models.PROTECT)
diff --git a/src/shop/templates/product_detail.html b/src/shop/templates/product_detail.html
index 25799e63..e73081c4 100644
--- a/src/shop/templates/product_detail.html
+++ b/src/shop/templates/product_detail.html
@@ -25,15 +25,21 @@
- {% if product.stock_amount %}
+ {% if product.stock_amount and product.is_time_available %}
Availability
+ {% if product.left_in_stock > 0 %}
{{ product.left_in_stock }} available
+ {% else %}
+ Sold out.
+ {% endif %}
{% endif %}
+ {% if product.is_stock_available %}
+
Add to order
{% if user.is_authenticated %}
@@ -67,6 +73,8 @@
to order this product
{% endif %}
+ {% endif %}
+
diff --git a/src/shop/templates/shop_index.html b/src/shop/templates/shop_index.html
index b9f805ba..c0915e5d 100644
--- a/src/shop/templates/shop_index.html
+++ b/src/shop/templates/shop_index.html
@@ -47,7 +47,6 @@ Shop | {{ block.super }}
-
{% endifchanged %}
@@ -56,6 +55,11 @@ Shop | {{ block.super }}
{{ product.name }}
+ {% if product.stock_amount and product.left_in_stock <= 10 %}
+
+ Only {{ product.left_in_stock }} left!
+
+ {% endif %}
diff --git a/src/shop/tests.py b/src/shop/tests.py
index daca2dd5..809f2a5e 100644
--- a/src/shop/tests.py
+++ b/src/shop/tests.py
@@ -1,4 +1,7 @@
from django.test import TestCase
+from django.utils import timezone
+
+from psycopg2.extras import DateTimeTZRange
from .factories import (
ProductFactory,
@@ -13,8 +16,8 @@ class ProductAvailabilityTest(TestCase):
def test_product_available_by_stock(self):
""" If no orders have been made, the product is still available. """
product = ProductFactory(stock_amount=10)
- self.assertEqual(product.left_in_stock(), 10)
- self.assertTrue(product.is_available)
+ self.assertEqual(product.left_in_stock, 10)
+ self.assertTrue(product.is_available())
def test_product_not_available_by_stock(self):
""" If max orders have been made, the product is NOT available. """
@@ -26,5 +29,48 @@ class ProductAvailabilityTest(TestCase):
order.paid = True
order.save()
- self.assertEqual(product.left_in_stock(), 0)
+ self.assertEqual(product.left_in_stock, 0)
+ self.assertFalse(product.is_stock_available)
self.assertFalse(product.is_available())
+
+ def test_product_available_by_time(self):
+ """ The product is available if now is in the right timeframe. """
+ product = ProductFactory()
+ # The factory defines the timeframe as now and 31 days forward.
+ self.assertTrue(product.is_time_available)
+ self.assertTrue(product.is_available())
+
+ def test_product_not_available_by_time(self):
+ """ The product is not available if now is outside the timeframe. """
+ available_in = DateTimeTZRange(
+ lower=timezone.now() - timezone.timedelta(5),
+ upper=timezone.now() - timezone.timedelta(1)
+ )
+ product = ProductFactory(available_in=available_in)
+ # The factory defines the timeframe as now and 31 days forward.
+ self.assertFalse(product.is_time_available)
+ self.assertFalse(product.is_available())
+
+ def test_product_is_not_available_yet(self):
+ """ The product is not available because we are before lower bound. """
+ available_in = DateTimeTZRange(
+ lower=timezone.now() + timezone.timedelta(5)
+ )
+ product = ProductFactory(available_in=available_in)
+ # Make sure there is no upper - just in case.
+ self.assertEqual(product.available_in.upper, None)
+ # The factory defines the timeframe as now and 31 days forward.
+ self.assertFalse(product.is_time_available)
+ self.assertFalse(product.is_available())
+
+ def test_product_is_available_from_now_on(self):
+ """ The product is available because we are after lower bound. """
+ available_in = DateTimeTZRange(
+ lower=timezone.now() - timezone.timedelta(1)
+ )
+ product = ProductFactory(available_in=available_in)
+ # Make sure there is no upper - just in case.
+ self.assertEqual(product.available_in.upper, None)
+ # The factory defines the timeframe as now and 31 days forward.
+ self.assertTrue(product.is_time_available)
+ self.assertTrue(product.is_available())
|