Merge pull request #221 from bornhack/add_tests_to_shop
WIP Fix shop stock code and add tests.
This commit is contained in:
commit
35f01dddbc
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -25,15 +25,21 @@
|
|||
|
||||
<hr />
|
||||
|
||||
{% if product.stock_amount %}
|
||||
{% if product.stock_amount and product.is_time_available %}
|
||||
<h3>
|
||||
<small>Availability</small><br />
|
||||
{% if product.left_in_stock > 0 %}
|
||||
<bold>{{ product.left_in_stock }}</bold> available<br />
|
||||
{% else %}
|
||||
<bold>Sold out.</bold>
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
<hr />
|
||||
{% endif %}
|
||||
|
||||
{% if product.is_stock_available %}
|
||||
|
||||
<h3>Add to order</h3>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
|
@ -67,6 +73,8 @@
|
|||
to order this product
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -47,7 +47,6 @@ Shop | {{ block.super }}
|
|||
</a>
|
||||
</strong>
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
{% endifchanged %}
|
||||
|
||||
|
@ -56,6 +55,11 @@ Shop | {{ block.super }}
|
|||
<a href="{% url 'shop:product_detail' slug=product.slug %}">
|
||||
{{ product.name }}
|
||||
</a>
|
||||
{% if product.stock_amount and product.left_in_stock <= 10 %}
|
||||
<div class="label label-danger">
|
||||
Only {{ product.left_in_stock }} left!
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="pull-right">
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from psycopg2.extras import DateTimeTZRange
|
||||
|
||||
from .factories import (
|
||||
ProductFactory,
|
||||
OrderFactory,
|
||||
OrderProductRelationFactory,
|
||||
)
|
||||
|
||||
|
@ -13,8 +15,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 +28,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())
|
||||
|
|
Loading…
Reference in a new issue