Merge branch 'show_available_discounts'
This commit is contained in:
commit
940bf803b6
9 changed files with 475 additions and 102 deletions
|
@ -1,8 +1,9 @@
|
|||
import datetime
|
||||
import discount
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max, Sum
|
||||
from django.db.models import Max
|
||||
from django.utils import timezone
|
||||
|
||||
from registrasion import models as rego
|
||||
|
@ -187,38 +188,47 @@ class CartController(object):
|
|||
# Delete the existing entries.
|
||||
rego.DiscountItem.objects.filter(cart=self.cart).delete()
|
||||
|
||||
product_items = self.cart.productitem_set.all()
|
||||
|
||||
products = [i.product for i in product_items]
|
||||
discounts = discount.available_discounts(self.cart.user, [], products)
|
||||
|
||||
# The highest-value discounts will apply to the highest-value
|
||||
# products first.
|
||||
product_items = self.cart.productitem_set.all()
|
||||
product_items = product_items.order_by('product__price')
|
||||
product_items = reversed(product_items)
|
||||
for item in product_items:
|
||||
self._add_discount(item.product, item.quantity)
|
||||
self._add_discount(item.product, item.quantity, discounts)
|
||||
|
||||
def _add_discount(self, product, quantity):
|
||||
''' Calculates the best available discounts for this product.
|
||||
NB this will be super-inefficient in aggregate because discounts will
|
||||
be re-tested for each product. We should work on that.'''
|
||||
def _add_discount(self, product, quantity, discounts):
|
||||
''' Applies the best discounts on the given product, from the given
|
||||
discounts.'''
|
||||
|
||||
prod = ProductController(product)
|
||||
discounts = prod.available_discounts(self.cart.user)
|
||||
discounts.sort(key=lambda discount: discount.value)
|
||||
def matches(discount):
|
||||
''' Returns True if and only if the given discount apples to
|
||||
our product. '''
|
||||
if isinstance(discount.clause, rego.DiscountForCategory):
|
||||
return discount.clause.category == product.category
|
||||
else:
|
||||
return discount.clause.product == product
|
||||
|
||||
for discount in reversed(discounts):
|
||||
def value(discount):
|
||||
''' Returns the value of this discount clause
|
||||
as applied to this product '''
|
||||
if discount.clause.percentage is not None:
|
||||
return discount.clause.percentage * product.price
|
||||
else:
|
||||
return discount.clause.price
|
||||
|
||||
discounts = [i for i in discounts if matches(i)]
|
||||
discounts.sort(key=value)
|
||||
|
||||
for candidate in reversed(discounts):
|
||||
if quantity == 0:
|
||||
break
|
||||
|
||||
# Get the count of past uses of this discount condition
|
||||
# as this affects the total amount we're allowed to use now.
|
||||
past_uses = rego.DiscountItem.objects.filter(
|
||||
cart__user=self.cart.user,
|
||||
discount=discount.discount,
|
||||
)
|
||||
agg = past_uses.aggregate(Sum("quantity"))
|
||||
past_uses = agg["quantity__sum"]
|
||||
if past_uses is None:
|
||||
past_uses = 0
|
||||
if past_uses == discount.condition.quantity:
|
||||
elif candidate.quantity == 0:
|
||||
# This discount clause has been exhausted by this cart
|
||||
continue
|
||||
|
||||
# Get a provisional instance for this DiscountItem
|
||||
|
@ -226,13 +236,13 @@ class CartController(object):
|
|||
discount_item = rego.DiscountItem.objects.create(
|
||||
product=product,
|
||||
cart=self.cart,
|
||||
discount=discount.discount,
|
||||
discount=candidate.discount,
|
||||
quantity=quantity,
|
||||
)
|
||||
|
||||
# Truncate the quantity for this DiscountItem if we exceed quantity
|
||||
ours = discount_item.quantity
|
||||
allowed = discount.condition.quantity - past_uses
|
||||
allowed = candidate.quantity
|
||||
if ours > allowed:
|
||||
discount_item.quantity = allowed
|
||||
# Update the remaining quantity.
|
||||
|
@ -240,4 +250,6 @@ class CartController(object):
|
|||
else:
|
||||
quantity = 0
|
||||
|
||||
candidate.quantity -= discount_item.quantity
|
||||
|
||||
discount_item.save()
|
||||
|
|
83
registrasion/controllers/discount.py
Normal file
83
registrasion/controllers/discount.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
import itertools
|
||||
|
||||
from conditions import ConditionController
|
||||
from registrasion import models as rego
|
||||
|
||||
from django.db.models import Sum
|
||||
|
||||
|
||||
class DiscountAndQuantity(object):
|
||||
def __init__(self, discount, clause, quantity):
|
||||
self.discount = discount
|
||||
self.clause = clause
|
||||
self.quantity = quantity
|
||||
|
||||
|
||||
def available_discounts(user, categories, products):
|
||||
''' Returns all discounts available to this user for the given categories
|
||||
and products. The discounts also list the available quantity for this user,
|
||||
not including products that are pending purchase. '''
|
||||
|
||||
# discounts that match provided categories
|
||||
category_discounts = rego.DiscountForCategory.objects.filter(
|
||||
category__in=categories
|
||||
)
|
||||
# discounts that match provided products
|
||||
product_discounts = rego.DiscountForProduct.objects.filter(
|
||||
product__in=products
|
||||
)
|
||||
# discounts that match categories for provided products
|
||||
product_category_discounts = rego.DiscountForCategory.objects.filter(
|
||||
category__in=(product.category for product in products)
|
||||
)
|
||||
# (Not relevant: discounts that match products in provided categories)
|
||||
|
||||
# The set of all potential discounts
|
||||
potential_discounts = set(itertools.chain(
|
||||
product_discounts,
|
||||
category_discounts,
|
||||
product_category_discounts,
|
||||
))
|
||||
|
||||
discounts = []
|
||||
|
||||
# Markers so that we don't need to evaluate given conditions more than once
|
||||
accepted_discounts = set()
|
||||
failed_discounts = set()
|
||||
|
||||
for discount in potential_discounts:
|
||||
real_discount = rego.DiscountBase.objects.get_subclass(
|
||||
pk=discount.discount.pk,
|
||||
)
|
||||
cond = ConditionController.for_condition(real_discount)
|
||||
|
||||
# Count the past uses of the given discount item.
|
||||
# If this user has exceeded the limit for the clause, this clause
|
||||
# is not available any more.
|
||||
past_uses = rego.DiscountItem.objects.filter(
|
||||
cart__user=user,
|
||||
cart__active=False, # Only past carts count
|
||||
discount=discount.discount,
|
||||
)
|
||||
agg = past_uses.aggregate(Sum("quantity"))
|
||||
past_use_count = agg["quantity__sum"]
|
||||
if past_use_count is None:
|
||||
past_use_count = 0
|
||||
|
||||
if past_use_count >= discount.quantity:
|
||||
# This clause has exceeded its use count
|
||||
pass
|
||||
elif real_discount not in failed_discounts:
|
||||
# This clause is still available
|
||||
if real_discount in accepted_discounts or cond.is_met(user, 0):
|
||||
# This clause is valid for this user
|
||||
discounts.append(DiscountAndQuantity(
|
||||
discount=real_discount,
|
||||
clause=discount,
|
||||
quantity=discount.quantity - past_use_count,
|
||||
))
|
||||
accepted_discounts.add(real_discount)
|
||||
else:
|
||||
# This clause is not valid for this user
|
||||
failed_discounts.add(real_discount)
|
||||
return discounts
|
|
@ -1,24 +1,39 @@
|
|||
import itertools
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from django.db.models import Q
|
||||
from registrasion import models as rego
|
||||
|
||||
from conditions import ConditionController
|
||||
|
||||
DiscountEnabler = namedtuple(
|
||||
"DiscountEnabler", (
|
||||
"discount",
|
||||
"condition",
|
||||
"value"))
|
||||
|
||||
|
||||
class ProductController(object):
|
||||
|
||||
def __init__(self, product):
|
||||
self.product = product
|
||||
|
||||
@classmethod
|
||||
def available_products(cls, user, category=None, products=None):
|
||||
''' Returns a list of all of the products that are available per
|
||||
enabling conditions from the given categories.
|
||||
TODO: refactor so that all conditions are tested here and
|
||||
can_add_with_enabling_conditions calls this method. '''
|
||||
if category is None and products is None:
|
||||
raise ValueError("You must provide products or a category")
|
||||
|
||||
if category is not None:
|
||||
all_products = rego.Product.objects.filter(category=category)
|
||||
else:
|
||||
all_products = []
|
||||
|
||||
if products is not None:
|
||||
all_products = itertools.chain(all_products, products)
|
||||
|
||||
return [
|
||||
product
|
||||
for product in all_products
|
||||
if cls(product).can_add_with_enabling_conditions(user, 0)
|
||||
]
|
||||
|
||||
def user_can_add_within_limit(self, user, quantity):
|
||||
''' Return true if the user is able to add _quantity_ to their count of
|
||||
this Product without exceeding _limit_per_user_.'''
|
||||
|
@ -68,39 +83,3 @@ class ProductController(object):
|
|||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_enabler(self, condition):
|
||||
if condition.percentage is not None:
|
||||
value = condition.percentage * self.product.price
|
||||
else:
|
||||
value = condition.price
|
||||
return DiscountEnabler(
|
||||
discount=condition.discount,
|
||||
condition=condition,
|
||||
value=value
|
||||
)
|
||||
|
||||
def available_discounts(self, user):
|
||||
''' Returns the set of available discounts for this user, for this
|
||||
product. '''
|
||||
|
||||
product_discounts = rego.DiscountForProduct.objects.filter(
|
||||
product=self.product)
|
||||
category_discounts = rego.DiscountForCategory.objects.filter(
|
||||
category=self.product.category
|
||||
)
|
||||
|
||||
potential_discounts = set(itertools.chain(
|
||||
(self.get_enabler(i) for i in product_discounts),
|
||||
(self.get_enabler(i) for i in category_discounts),
|
||||
))
|
||||
|
||||
discounts = []
|
||||
for discount in potential_discounts:
|
||||
real_discount = rego.DiscountBase.objects.get_subclass(
|
||||
pk=discount.discount.pk)
|
||||
cond = ConditionController.for_condition(real_discount)
|
||||
if cond.is_met(user, 0):
|
||||
discounts.append(discount)
|
||||
|
||||
return discounts
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
import models as rego
|
||||
|
||||
from controllers.product import ProductController
|
||||
|
||||
from django import forms
|
||||
|
||||
|
||||
def CategoryForm(category):
|
||||
def ProductsForm(products):
|
||||
|
||||
PREFIX = "product_"
|
||||
|
||||
def field_name(product):
|
||||
return PREFIX + ("%d" % product.id)
|
||||
|
||||
class _CategoryForm(forms.Form):
|
||||
class _ProductsForm(forms.Form):
|
||||
|
||||
@staticmethod
|
||||
def initial_data(product_quantities):
|
||||
def __init__(self, *a, **k):
|
||||
if "product_quantities" in k:
|
||||
initial = _ProductsForm.initial_data(k["product_quantities"])
|
||||
k["initial"] = initial
|
||||
del k["product_quantities"]
|
||||
super(_ProductsForm, self).__init__(*a, **k)
|
||||
|
||||
@classmethod
|
||||
def initial_data(cls, product_quantities):
|
||||
''' Prepares initial data for an instance of this form.
|
||||
product_quantities is a sequence of (product,quantity) tuples '''
|
||||
initial = {}
|
||||
|
@ -32,18 +37,6 @@ def CategoryForm(category):
|
|||
product_id = int(name[len(PREFIX):])
|
||||
yield (product_id, value, name)
|
||||
|
||||
def disable_product(self, product):
|
||||
''' Removes a given product from this form. '''
|
||||
del self.fields[field_name(product)]
|
||||
|
||||
def disable_products_for_user(self, user):
|
||||
for product in products:
|
||||
# Remove fields that do not have an enabling condition.
|
||||
prod = ProductController(product)
|
||||
if not prod.can_add_with_enabling_conditions(user, 0):
|
||||
self.disable_product(product)
|
||||
|
||||
products = rego.Product.objects.filter(category=category).order_by("order")
|
||||
for product in products:
|
||||
|
||||
help_text = "$%d -- %s" % (product.price, product.description)
|
||||
|
@ -52,9 +45,9 @@ def CategoryForm(category):
|
|||
label=product.name,
|
||||
help_text=help_text,
|
||||
)
|
||||
_CategoryForm.base_fields[field_name(product)] = field
|
||||
_ProductsForm.base_fields[field_name(product)] = field
|
||||
|
||||
return _CategoryForm
|
||||
return _ProductsForm
|
||||
|
||||
|
||||
class ProfileForm(forms.ModelForm):
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
|
||||
<h1>Product Category: {{ category.name }}</h1>
|
||||
|
||||
<p>{{ category.description }}</p>
|
||||
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
|
||||
|
@ -14,13 +12,37 @@
|
|||
{{ voucher_form }}
|
||||
</table>
|
||||
|
||||
<input type="submit">
|
||||
<p><input type="submit"></p>
|
||||
|
||||
{% if discounts %}
|
||||
<h3>Available Discounts</h3>
|
||||
<ul>
|
||||
{% for discount in discounts %}
|
||||
<li>{{ discount.quantity }} x
|
||||
{% if discount.clause.percentage %}
|
||||
{{ discount.clause.percentage|floatformat:"2" }}%
|
||||
{% else %}
|
||||
${{ discount.clause.price|floatformat:"2" }}
|
||||
{% endif %}
|
||||
off
|
||||
{% if discount.clause.category %}
|
||||
{{ discount.clause.category }}
|
||||
{% else %}
|
||||
{{ discount.clause.product.category }}
|
||||
- {{ discount.clause.product }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h3>Available Products</h3>
|
||||
<p>{{ category.description }}</p>
|
||||
<table>
|
||||
{{ form }}
|
||||
</table>
|
||||
|
||||
<input type="submit">
|
||||
<p><input type="submit"></p>
|
||||
|
||||
</form>
|
||||
|
||||
|
|
|
@ -1,11 +1,47 @@
|
|||
from registrasion import models as rego
|
||||
|
||||
from collections import namedtuple
|
||||
from django import template
|
||||
from django.db.models import Sum
|
||||
|
||||
register = template.Library()
|
||||
|
||||
ProductAndQuantity = namedtuple("ProductAndQuantity", ["product", "quantity"])
|
||||
|
||||
@register.assignment_tag(takes_context=True)
|
||||
def available_categories(context):
|
||||
''' Returns all of the available product categories '''
|
||||
return rego.Category.objects.all()
|
||||
|
||||
@register.assignment_tag(takes_context=True)
|
||||
def invoices(context):
|
||||
''' Returns all of the invoices that this user has. '''
|
||||
return rego.Invoice.objects.filter(cart__user=context.request.user)
|
||||
|
||||
@register.assignment_tag(takes_context=True)
|
||||
def items_pending(context):
|
||||
''' Returns all of the items that this user has in their current cart,
|
||||
and is awaiting payment. '''
|
||||
|
||||
all_items = rego.ProductItem.objects.filter(
|
||||
cart__user=context.request.user,
|
||||
cart__active=True,
|
||||
)
|
||||
return all_items
|
||||
|
||||
@register.assignment_tag(takes_context=True)
|
||||
def items_purchased(context):
|
||||
''' Returns all of the items that this user has purchased '''
|
||||
|
||||
all_items = rego.ProductItem.objects.filter(
|
||||
cart__user=context.request.user,
|
||||
cart__active=False,
|
||||
)
|
||||
|
||||
products = set(item.product for item in all_items)
|
||||
out = []
|
||||
for product in products:
|
||||
pp = all_items.filter(product=product)
|
||||
quantity = pp.aggregate(Sum("quantity"))["quantity__sum"]
|
||||
out.append(ProductAndQuantity(product, quantity))
|
||||
return out
|
||||
|
|
|
@ -3,7 +3,9 @@ import pytz
|
|||
from decimal import Decimal
|
||||
|
||||
from registrasion import models as rego
|
||||
from registrasion.controllers import discount
|
||||
from registrasion.controllers.cart import CartController
|
||||
from registrasion.controllers.invoice import InvoiceController
|
||||
|
||||
from test_cart import RegistrationCartTestCase
|
||||
|
||||
|
@ -13,7 +15,11 @@ UTC = pytz.timezone('UTC')
|
|||
class DiscountTestCase(RegistrationCartTestCase):
|
||||
|
||||
@classmethod
|
||||
def add_discount_prod_1_includes_prod_2(cls, amount=Decimal(100)):
|
||||
def add_discount_prod_1_includes_prod_2(
|
||||
cls,
|
||||
amount=Decimal(100),
|
||||
quantity=2,
|
||||
):
|
||||
discount = rego.IncludedProductDiscount.objects.create(
|
||||
description="PROD_1 includes PROD_2 " + str(amount) + "%",
|
||||
)
|
||||
|
@ -24,7 +30,7 @@ class DiscountTestCase(RegistrationCartTestCase):
|
|||
discount=discount,
|
||||
product=cls.PROD_2,
|
||||
percentage=amount,
|
||||
quantity=2
|
||||
quantity=quantity,
|
||||
).save()
|
||||
return discount
|
||||
|
||||
|
@ -32,7 +38,8 @@ class DiscountTestCase(RegistrationCartTestCase):
|
|||
def add_discount_prod_1_includes_cat_2(
|
||||
cls,
|
||||
amount=Decimal(100),
|
||||
quantity=2):
|
||||
quantity=2,
|
||||
):
|
||||
discount = rego.IncludedProductDiscount.objects.create(
|
||||
description="PROD_1 includes CAT_2 " + str(amount) + "%",
|
||||
)
|
||||
|
@ -47,6 +54,33 @@ class DiscountTestCase(RegistrationCartTestCase):
|
|||
).save()
|
||||
return discount
|
||||
|
||||
@classmethod
|
||||
def add_discount_prod_1_includes_prod_3_and_prod_4(
|
||||
cls,
|
||||
amount=Decimal(100),
|
||||
quantity=2,
|
||||
):
|
||||
discount = rego.IncludedProductDiscount.objects.create(
|
||||
description="PROD_1 includes PROD_3 and PROD_4 " +
|
||||
str(amount) + "%",
|
||||
)
|
||||
discount.save()
|
||||
discount.enabling_products.add(cls.PROD_1)
|
||||
discount.save()
|
||||
rego.DiscountForProduct.objects.create(
|
||||
discount=discount,
|
||||
product=cls.PROD_3,
|
||||
percentage=amount,
|
||||
quantity=quantity,
|
||||
).save()
|
||||
rego.DiscountForProduct.objects.create(
|
||||
discount=discount,
|
||||
product=cls.PROD_4,
|
||||
percentage=amount,
|
||||
quantity=quantity,
|
||||
).save()
|
||||
return discount
|
||||
|
||||
def test_discount_is_applied(self):
|
||||
self.add_discount_prod_1_includes_prod_2()
|
||||
|
||||
|
@ -214,3 +248,132 @@ class DiscountTestCase(RegistrationCartTestCase):
|
|||
discount_items = list(cart.cart.discountitem_set.all())
|
||||
# The discount is applied.
|
||||
self.assertEqual(1, len(discount_items))
|
||||
|
||||
# Tests for the discount.available_discounts enumerator
|
||||
def test_enumerate_no_discounts_for_no_input(self):
|
||||
discounts = discount.available_discounts(self.USER_1, [], [])
|
||||
self.assertEqual(0, len(discounts))
|
||||
|
||||
def test_enumerate_no_discounts_if_condition_not_met(self):
|
||||
self.add_discount_prod_1_includes_cat_2(quantity=1)
|
||||
|
||||
discounts = discount.available_discounts(
|
||||
self.USER_1,
|
||||
[],
|
||||
[self.PROD_3],
|
||||
)
|
||||
self.assertEqual(0, len(discounts))
|
||||
|
||||
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], [])
|
||||
self.assertEqual(0, len(discounts))
|
||||
|
||||
def test_category_discount_appears_once_if_met_twice(self):
|
||||
self.add_discount_prod_1_includes_cat_2(quantity=1)
|
||||
|
||||
cart = CartController.for_user(self.USER_1)
|
||||
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
|
||||
|
||||
discounts = discount.available_discounts(
|
||||
self.USER_1,
|
||||
[self.CAT_2],
|
||||
[self.PROD_3],
|
||||
)
|
||||
self.assertEqual(1, len(discounts))
|
||||
|
||||
def test_category_discount_appears_with_category(self):
|
||||
self.add_discount_prod_1_includes_cat_2(quantity=1)
|
||||
|
||||
cart = CartController.for_user(self.USER_1)
|
||||
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
|
||||
|
||||
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], [])
|
||||
self.assertEqual(1, len(discounts))
|
||||
|
||||
def test_category_discount_appears_with_product(self):
|
||||
self.add_discount_prod_1_includes_cat_2(quantity=1)
|
||||
|
||||
cart = CartController.for_user(self.USER_1)
|
||||
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
|
||||
|
||||
discounts = discount.available_discounts(
|
||||
self.USER_1,
|
||||
[],
|
||||
[self.PROD_3],
|
||||
)
|
||||
self.assertEqual(1, len(discounts))
|
||||
|
||||
def test_category_discount_appears_once_with_two_valid_product(self):
|
||||
self.add_discount_prod_1_includes_cat_2(quantity=1)
|
||||
|
||||
cart = CartController.for_user(self.USER_1)
|
||||
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
|
||||
|
||||
discounts = discount.available_discounts(
|
||||
self.USER_1,
|
||||
[],
|
||||
[self.PROD_3, self.PROD_4]
|
||||
)
|
||||
self.assertEqual(1, len(discounts))
|
||||
|
||||
def test_product_discount_appears_with_product(self):
|
||||
self.add_discount_prod_1_includes_prod_2(quantity=1)
|
||||
|
||||
cart = CartController.for_user(self.USER_1)
|
||||
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
|
||||
|
||||
discounts = discount.available_discounts(
|
||||
self.USER_1,
|
||||
[],
|
||||
[self.PROD_2],
|
||||
)
|
||||
self.assertEqual(1, len(discounts))
|
||||
|
||||
def test_product_discount_does_not_appear_with_category(self):
|
||||
self.add_discount_prod_1_includes_prod_2(quantity=1)
|
||||
|
||||
cart = CartController.for_user(self.USER_1)
|
||||
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
|
||||
|
||||
discounts = discount.available_discounts(self.USER_1, [self.CAT_1], [])
|
||||
self.assertEqual(0, len(discounts))
|
||||
|
||||
def test_discount_quantity_is_correct_before_first_purchase(self):
|
||||
self.add_discount_prod_1_includes_cat_2(quantity=2)
|
||||
|
||||
cart = CartController.for_user(self.USER_1)
|
||||
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
|
||||
cart.add_to_cart(self.PROD_3, 1) # Exhaust the quantity
|
||||
|
||||
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], [])
|
||||
self.assertEqual(2, discounts[0].quantity)
|
||||
inv = InvoiceController.for_cart(cart.cart)
|
||||
inv.pay("Dummy reference", inv.invoice.value)
|
||||
self.assertTrue(inv.invoice.paid)
|
||||
|
||||
def test_discount_quantity_is_correct_after_first_purchase(self):
|
||||
self.test_discount_quantity_is_correct_before_first_purchase()
|
||||
|
||||
cart = CartController.for_user(self.USER_1)
|
||||
cart.add_to_cart(self.PROD_3, 1) # Exhaust the quantity
|
||||
|
||||
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], [])
|
||||
self.assertEqual(1, discounts[0].quantity)
|
||||
inv = InvoiceController.for_cart(cart.cart)
|
||||
inv.pay("Dummy reference", inv.invoice.value)
|
||||
self.assertTrue(inv.invoice.paid)
|
||||
|
||||
def test_discount_is_gone_after_quantity_exhausted(self):
|
||||
self.test_discount_quantity_is_correct_after_first_purchase()
|
||||
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], [])
|
||||
self.assertEqual(0, len(discounts))
|
||||
|
||||
def test_product_discount_enabled_twice_appears_twice(self):
|
||||
self.add_discount_prod_1_includes_prod_3_and_prod_4(quantity=2)
|
||||
cart = CartController.for_user(self.USER_1)
|
||||
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
|
||||
discounts = discount.available_discounts(
|
||||
self.USER_1,
|
||||
[],
|
||||
[self.PROD_3, self.PROD_4],
|
||||
)
|
||||
self.assertEqual(2, len(discounts))
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
|
|||
|
||||
from registrasion import models as rego
|
||||
from registrasion.controllers.cart import CartController
|
||||
from registrasion.controllers.product import ProductController
|
||||
|
||||
from test_cart import RegistrationCartTestCase
|
||||
|
||||
|
@ -155,3 +156,80 @@ class EnablingConditionTestCases(RegistrationCartTestCase):
|
|||
cart_1.add_to_cart(self.PROD_1, 1)
|
||||
cart_1.add_to_cart(self.PROD_3, 1) # Meets the category condition
|
||||
cart_1.add_to_cart(self.PROD_1, 1)
|
||||
|
||||
def test_available_products_works_with_no_conditions_set(self):
|
||||
prods = ProductController.available_products(
|
||||
self.USER_1,
|
||||
category=self.CAT_1,
|
||||
)
|
||||
|
||||
self.assertTrue(self.PROD_1 in prods)
|
||||
self.assertTrue(self.PROD_2 in prods)
|
||||
|
||||
prods = ProductController.available_products(
|
||||
self.USER_1,
|
||||
category=self.CAT_2,
|
||||
)
|
||||
|
||||
self.assertTrue(self.PROD_3 in prods)
|
||||
self.assertTrue(self.PROD_4 in prods)
|
||||
|
||||
prods = ProductController.available_products(
|
||||
self.USER_1,
|
||||
products=[self.PROD_1, self.PROD_2, self.PROD_3, self.PROD_4],
|
||||
)
|
||||
|
||||
self.assertTrue(self.PROD_1 in prods)
|
||||
self.assertTrue(self.PROD_2 in prods)
|
||||
self.assertTrue(self.PROD_3 in prods)
|
||||
self.assertTrue(self.PROD_4 in prods)
|
||||
|
||||
def test_available_products_on_category_works_when_condition_not_met(self):
|
||||
self.add_product_enabling_condition(mandatory=False)
|
||||
|
||||
prods = ProductController.available_products(
|
||||
self.USER_1,
|
||||
category=self.CAT_1,
|
||||
)
|
||||
|
||||
self.assertTrue(self.PROD_1 not in prods)
|
||||
self.assertTrue(self.PROD_2 in prods)
|
||||
|
||||
def test_available_products_on_category_works_when_condition_is_met(self):
|
||||
self.add_product_enabling_condition(mandatory=False)
|
||||
|
||||
cart_1 = CartController.for_user(self.USER_1)
|
||||
cart_1.add_to_cart(self.PROD_2, 1)
|
||||
|
||||
prods = ProductController.available_products(
|
||||
self.USER_1,
|
||||
category=self.CAT_1,
|
||||
)
|
||||
|
||||
self.assertTrue(self.PROD_1 in prods)
|
||||
self.assertTrue(self.PROD_2 in prods)
|
||||
|
||||
def test_available_products_on_products_works_when_condition_not_met(self):
|
||||
self.add_product_enabling_condition(mandatory=False)
|
||||
|
||||
prods = ProductController.available_products(
|
||||
self.USER_1,
|
||||
products=[self.PROD_1, self.PROD_2],
|
||||
)
|
||||
|
||||
self.assertTrue(self.PROD_1 not in prods)
|
||||
self.assertTrue(self.PROD_2 in prods)
|
||||
|
||||
def test_available_products_on_products_works_when_condition_is_met(self):
|
||||
self.add_product_enabling_condition(mandatory=False)
|
||||
|
||||
cart_1 = CartController.for_user(self.USER_1)
|
||||
cart_1.add_to_cart(self.PROD_2, 1)
|
||||
|
||||
prods = ProductController.available_products(
|
||||
self.USER_1,
|
||||
products=[self.PROD_1, self.PROD_2],
|
||||
)
|
||||
|
||||
self.assertTrue(self.PROD_1 in prods)
|
||||
self.assertTrue(self.PROD_2 in prods)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from registrasion import forms
|
||||
from registrasion import models as rego
|
||||
from registrasion.controllers import discount
|
||||
from registrasion.controllers.cart import CartController
|
||||
from registrasion.controllers.invoice import InvoiceController
|
||||
from registrasion.controllers.product import ProductController
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
@ -95,19 +97,21 @@ def product_category(request, category_id):
|
|||
category = rego.Category.objects.get(pk=category_id)
|
||||
current_cart = CartController.for_user(request.user)
|
||||
|
||||
CategoryForm = forms.CategoryForm(category)
|
||||
|
||||
attendee = rego.Attendee.get_instance(request.user)
|
||||
|
||||
products = rego.Product.objects.filter(category=category)
|
||||
products = products.order_by("order")
|
||||
products = ProductController.available_products(
|
||||
request.user,
|
||||
products=products,
|
||||
)
|
||||
ProductsForm = forms.ProductsForm(products)
|
||||
|
||||
if request.method == "POST":
|
||||
cat_form = CategoryForm(
|
||||
cat_form = ProductsForm(
|
||||
request.POST,
|
||||
request.FILES,
|
||||
prefix=PRODUCTS_FORM_PREFIX)
|
||||
cat_form.disable_products_for_user(request.user)
|
||||
voucher_form = forms.VoucherForm(
|
||||
request.POST,
|
||||
prefix=VOUCHERS_FORM_PREFIX)
|
||||
|
@ -165,14 +169,17 @@ def product_category(request, category_id):
|
|||
quantity = 0
|
||||
quantities.append((product, quantity))
|
||||
|
||||
initial = CategoryForm.initial_data(quantities)
|
||||
cat_form = CategoryForm(prefix=PRODUCTS_FORM_PREFIX, initial=initial)
|
||||
cat_form.disable_products_for_user(request.user)
|
||||
cat_form = ProductsForm(
|
||||
prefix=PRODUCTS_FORM_PREFIX,
|
||||
product_quantities=quantities,
|
||||
)
|
||||
|
||||
voucher_form = forms.VoucherForm(prefix=VOUCHERS_FORM_PREFIX)
|
||||
|
||||
discounts = discount.available_discounts(request.user, [], products)
|
||||
data = {
|
||||
"category": category,
|
||||
"discounts": discounts,
|
||||
"form": cat_form,
|
||||
"voucher_form": voucher_form,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue