Merge branch 'category_form'
This commit is contained in:
commit
2f4ebc22af
14 changed files with 473 additions and 60 deletions
|
@ -80,3 +80,15 @@ class VoucherAdmin(nested_admin.NestedAdmin):
|
||||||
VoucherDiscountInline,
|
VoucherDiscountInline,
|
||||||
VoucherEnablingConditionInline,
|
VoucherEnablingConditionInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Enabling conditions
|
||||||
|
@admin.register(rego.ProductEnablingCondition)
|
||||||
|
class ProductEnablingConditionAdmin(nested_admin.NestedAdmin):
|
||||||
|
model = rego.ProductEnablingCondition
|
||||||
|
|
||||||
|
|
||||||
|
# Enabling conditions
|
||||||
|
@admin.register(rego.CategoryEnablingCondition)
|
||||||
|
class CategoryEnablingConditionAdmin(nested_admin.NestedAdmin):
|
||||||
|
model = rego.CategoryEnablingCondition
|
||||||
|
|
|
@ -54,57 +54,91 @@ class CartController(object):
|
||||||
self.cart.time_last_updated = timezone.now()
|
self.cart.time_last_updated = timezone.now()
|
||||||
self.cart.reservation_duration = max(reservations)
|
self.cart.reservation_duration = max(reservations)
|
||||||
|
|
||||||
def add_to_cart(self, product, quantity):
|
def end_batch(self):
|
||||||
''' Adds _quantity_ of the given _product_ to the cart. Raises
|
''' Performs operations that occur occur at the end of a batch of
|
||||||
ValidationError if constraints are violated.'''
|
product changes/voucher applications etc. '''
|
||||||
|
|
||||||
prod = ProductController(product)
|
|
||||||
|
|
||||||
# TODO: Check enabling conditions for product for user
|
|
||||||
|
|
||||||
if not prod.can_add_with_enabling_conditions(self.cart.user, quantity):
|
|
||||||
raise ValidationError("Not enough of that product left (ec)")
|
|
||||||
|
|
||||||
if not prod.user_can_add_within_limit(self.cart.user, quantity):
|
|
||||||
raise ValidationError("Not enough of that product left (user)")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Try to update an existing item within this cart if possible.
|
|
||||||
product_item = rego.ProductItem.objects.get(
|
|
||||||
cart=self.cart,
|
|
||||||
product=product)
|
|
||||||
product_item.quantity += quantity
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
product_item = rego.ProductItem.objects.create(
|
|
||||||
cart=self.cart,
|
|
||||||
product=product,
|
|
||||||
quantity=quantity,
|
|
||||||
)
|
|
||||||
product_item.save()
|
|
||||||
|
|
||||||
self.recalculate_discounts()
|
self.recalculate_discounts()
|
||||||
|
|
||||||
self.extend_reservation()
|
self.extend_reservation()
|
||||||
self.cart.revision += 1
|
self.cart.revision += 1
|
||||||
self.cart.save()
|
self.cart.save()
|
||||||
|
|
||||||
def apply_voucher(self, voucher):
|
def set_quantity(self, product, quantity, batched=False):
|
||||||
''' Applies the given voucher to this cart. '''
|
''' Sets the _quantity_ of the given _product_ in the cart to the given
|
||||||
|
_quantity_. '''
|
||||||
|
|
||||||
|
if quantity < 0:
|
||||||
|
raise ValidationError("Cannot have fewer than 0 items in cart.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
product_item = rego.ProductItem.objects.get(
|
||||||
|
cart=self.cart,
|
||||||
|
product=product)
|
||||||
|
old_quantity = product_item.quantity
|
||||||
|
|
||||||
|
if quantity == 0:
|
||||||
|
product_item.delete()
|
||||||
|
return
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
if quantity == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
product_item = rego.ProductItem.objects.create(
|
||||||
|
cart=self.cart,
|
||||||
|
product=product,
|
||||||
|
quantity=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
old_quantity = 0
|
||||||
|
|
||||||
|
# Validate the addition to the cart
|
||||||
|
adjustment = quantity - old_quantity
|
||||||
|
prod = ProductController(product)
|
||||||
|
|
||||||
|
if not prod.can_add_with_enabling_conditions(
|
||||||
|
self.cart.user, adjustment):
|
||||||
|
raise ValidationError("Not enough of that product left (ec)")
|
||||||
|
|
||||||
|
if not prod.user_can_add_within_limit(self.cart.user, adjustment):
|
||||||
|
raise ValidationError("Not enough of that product left (user)")
|
||||||
|
|
||||||
|
product_item.quantity = quantity
|
||||||
|
product_item.save()
|
||||||
|
|
||||||
|
if not batched:
|
||||||
|
self.end_batch()
|
||||||
|
|
||||||
|
def add_to_cart(self, product, quantity):
|
||||||
|
''' Adds _quantity_ of the given _product_ to the cart. Raises
|
||||||
|
ValidationError if constraints are violated.'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
product_item = rego.ProductItem.objects.get(
|
||||||
|
cart=self.cart,
|
||||||
|
product=product)
|
||||||
|
old_quantity = product_item.quantity
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
old_quantity = 0
|
||||||
|
self.set_quantity(product, old_quantity + quantity)
|
||||||
|
|
||||||
|
def apply_voucher(self, voucher_code):
|
||||||
|
''' Applies the voucher with the given code to this cart. '''
|
||||||
|
|
||||||
# TODO: is it valid for a cart to re-add a voucher that they have?
|
# TODO: is it valid for a cart to re-add a voucher that they have?
|
||||||
|
|
||||||
# Is voucher exhausted?
|
# Is voucher exhausted?
|
||||||
active_carts = rego.Cart.reserved_carts()
|
active_carts = rego.Cart.reserved_carts()
|
||||||
|
|
||||||
|
# Try and find the voucher
|
||||||
|
voucher = rego.Voucher.objects.get(code=voucher_code.upper())
|
||||||
|
|
||||||
carts_with_voucher = active_carts.filter(vouchers=voucher)
|
carts_with_voucher = active_carts.filter(vouchers=voucher)
|
||||||
if len(carts_with_voucher) >= voucher.limit:
|
if len(carts_with_voucher) >= voucher.limit:
|
||||||
raise ValidationError("This voucher is no longer available")
|
raise ValidationError("This voucher is no longer available")
|
||||||
|
|
||||||
# If successful...
|
# If successful...
|
||||||
self.cart.vouchers.add(voucher)
|
self.cart.vouchers.add(voucher)
|
||||||
|
self.end_batch()
|
||||||
self.extend_reservation()
|
|
||||||
self.cart.revision += 1
|
|
||||||
self.cart.save()
|
|
||||||
|
|
||||||
def validate_cart(self):
|
def validate_cart(self):
|
||||||
''' Determines whether the status of the current cart is valid;
|
''' Determines whether the status of the current cart is valid;
|
||||||
|
@ -153,7 +187,12 @@ class CartController(object):
|
||||||
# Delete the existing entries.
|
# Delete the existing entries.
|
||||||
rego.DiscountItem.objects.filter(cart=self.cart).delete()
|
rego.DiscountItem.objects.filter(cart=self.cart).delete()
|
||||||
|
|
||||||
for item in self.cart.productitem_set.all():
|
# 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)
|
||||||
|
|
||||||
def _add_discount(self, product, quantity):
|
def _add_discount(self, product, quantity):
|
||||||
|
@ -172,9 +211,7 @@ class CartController(object):
|
||||||
# Get the count of past uses of this discount condition
|
# Get the count of past uses of this discount condition
|
||||||
# as this affects the total amount we're allowed to use now.
|
# as this affects the total amount we're allowed to use now.
|
||||||
past_uses = rego.DiscountItem.objects.filter(
|
past_uses = rego.DiscountItem.objects.filter(
|
||||||
cart__active=False,
|
|
||||||
discount=discount.discount,
|
discount=discount.discount,
|
||||||
product=product,
|
|
||||||
)
|
)
|
||||||
agg = past_uses.aggregate(Sum("quantity"))
|
agg = past_uses.aggregate(Sum("quantity"))
|
||||||
past_uses = agg["quantity__sum"]
|
past_uses = agg["quantity__sum"]
|
||||||
|
|
|
@ -59,20 +59,23 @@ class InvoiceController(object):
|
||||||
|
|
||||||
# TODO: calculate line items.
|
# TODO: calculate line items.
|
||||||
product_items = rego.ProductItem.objects.filter(cart=cart)
|
product_items = rego.ProductItem.objects.filter(cart=cart)
|
||||||
|
product_items = product_items.order_by(
|
||||||
|
"product__category__order", "product__order"
|
||||||
|
)
|
||||||
discount_items = rego.DiscountItem.objects.filter(cart=cart)
|
discount_items = rego.DiscountItem.objects.filter(cart=cart)
|
||||||
invoice_value = Decimal()
|
invoice_value = Decimal()
|
||||||
for item in product_items:
|
for item in product_items:
|
||||||
|
product = item.product
|
||||||
line_item = rego.LineItem.objects.create(
|
line_item = rego.LineItem.objects.create(
|
||||||
invoice=invoice,
|
invoice=invoice,
|
||||||
description=item.product.name,
|
description="%s - %s" % (product.category.name, product.name),
|
||||||
quantity=item.quantity,
|
quantity=item.quantity,
|
||||||
price=item.product.price,
|
price=product.price,
|
||||||
)
|
)
|
||||||
line_item.save()
|
line_item.save()
|
||||||
invoice_value += line_item.quantity * line_item.price
|
invoice_value += line_item.quantity * line_item.price
|
||||||
|
|
||||||
for item in discount_items:
|
for item in discount_items:
|
||||||
|
|
||||||
line_item = rego.LineItem.objects.create(
|
line_item = rego.LineItem.objects.create(
|
||||||
invoice=invoice,
|
invoice=invoice,
|
||||||
description=item.discount.description,
|
description=item.discount.description,
|
||||||
|
|
55
registrasion/forms.py
Normal file
55
registrasion/forms.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import models as rego
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
def CategoryForm(category):
|
||||||
|
|
||||||
|
PREFIX = "product_"
|
||||||
|
|
||||||
|
def field_name(product):
|
||||||
|
return PREFIX + ("%d" % product.id)
|
||||||
|
|
||||||
|
class _CategoryForm(forms.Form):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initial_data(product_quantities):
|
||||||
|
''' Prepares initial data for an instance of this form.
|
||||||
|
product_quantities is a sequence of (product,quantity) tuples '''
|
||||||
|
initial = {}
|
||||||
|
for product, quantity in product_quantities:
|
||||||
|
initial[field_name(product)] = quantity
|
||||||
|
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def product_quantities(self):
|
||||||
|
''' Yields a sequence of (product, quantity) tuples from the
|
||||||
|
cleaned form data. '''
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith(PREFIX):
|
||||||
|
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)]
|
||||||
|
|
||||||
|
products = rego.Product.objects.filter(category=category).order_by("order")
|
||||||
|
for product in products:
|
||||||
|
|
||||||
|
help_text = "$%d -- %s" % (product.price, product.description)
|
||||||
|
|
||||||
|
field = forms.IntegerField(
|
||||||
|
label=product.name,
|
||||||
|
help_text=help_text,
|
||||||
|
)
|
||||||
|
_CategoryForm.base_fields[field_name(product)] = field
|
||||||
|
|
||||||
|
return _CategoryForm
|
||||||
|
|
||||||
|
class VoucherForm(forms.Form):
|
||||||
|
voucher = forms.CharField(
|
||||||
|
label="Voucher code",
|
||||||
|
help_text="If you have a voucher code, enter it here",
|
||||||
|
required=True,
|
||||||
|
)
|
|
@ -9,6 +9,8 @@ from django.conf import settings
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('registrasion', '0001_initial'), ('registrasion', '0002_auto_20160304_1723')]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
@ -225,12 +227,12 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='enablingconditionbase',
|
model_name='enablingconditionbase',
|
||||||
name='categories',
|
name='categories',
|
||||||
field=models.ManyToManyField(to=b'registrasion.Category'),
|
field=models.ManyToManyField(to=b'registrasion.Category', blank=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='enablingconditionbase',
|
model_name='enablingconditionbase',
|
||||||
name='products',
|
name='products',
|
||||||
field=models.ManyToManyField(to=b'registrasion.Product'),
|
field=models.ManyToManyField(to=b'registrasion.Product', blank=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='discountitem',
|
model_name='discountitem',
|
|
@ -99,6 +99,11 @@ class Voucher(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Voucher for %s" % self.recipient
|
return "Voucher for %s" % self.recipient
|
||||||
|
|
||||||
|
def save(self, *a, **k):
|
||||||
|
''' Normalise the voucher code to be uppercase '''
|
||||||
|
self.code = self.code.upper()
|
||||||
|
super(Voucher, self).save(*a, **k)
|
||||||
|
|
||||||
recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
|
recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
|
||||||
code = models.CharField(max_length=16,
|
code = models.CharField(max_length=16,
|
||||||
unique=True,
|
unique=True,
|
||||||
|
@ -149,13 +154,13 @@ class DiscountForProduct(models.Model):
|
||||||
cats = DiscountForCategory.objects.filter(
|
cats = DiscountForCategory.objects.filter(
|
||||||
discount=self.discount,
|
discount=self.discount,
|
||||||
category=self.product.category)
|
category=self.product.category)
|
||||||
if len(prods) > 1 or self not in prods:
|
if len(prods) > 1:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("You may only have one discount line per product"))
|
_("You may only have one discount line per product"))
|
||||||
if len(cats) != 0:
|
if len(cats) != 0:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("You may only have one discount for "
|
_("You may only have one discount for "
|
||||||
"a product or its category"))
|
"a product or its category"))
|
||||||
|
|
||||||
discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
|
discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
|
||||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||||
|
@ -184,8 +189,8 @@ class DiscountForCategory(models.Model):
|
||||||
if len(prods) != 0:
|
if len(prods) != 0:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("You may only have one discount for "
|
_("You may only have one discount for "
|
||||||
"a product or its category"))
|
"a product or its category"))
|
||||||
if len(cats) > 1 or self not in cats:
|
if len(cats) > 1:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("You may only have one discount line per category"))
|
_("You may only have one discount line per category"))
|
||||||
|
|
||||||
|
@ -257,8 +262,8 @@ class EnablingConditionBase(models.Model):
|
||||||
|
|
||||||
description = models.CharField(max_length=255)
|
description = models.CharField(max_length=255)
|
||||||
mandatory = models.BooleanField(default=False)
|
mandatory = models.BooleanField(default=False)
|
||||||
products = models.ManyToManyField(Product)
|
products = models.ManyToManyField(Product, blank=True)
|
||||||
categories = models.ManyToManyField(Category)
|
categories = models.ManyToManyField(Category, blank=True)
|
||||||
|
|
||||||
|
|
||||||
class TimeOrStockLimitEnablingCondition(EnablingConditionBase):
|
class TimeOrStockLimitEnablingCondition(EnablingConditionBase):
|
||||||
|
|
37
registrasion/templates/invoice.html
Normal file
37
registrasion/templates/invoice.html
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<!--- Sample template. Move elsewhere once it's ready to go. -->
|
||||||
|
|
||||||
|
{% extends "site_base.html" %}
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<h1>Invoice {{ invoice.id }}</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Void: {{ invoice.void }}</li>
|
||||||
|
<li>Paid: {{ invoice.paid }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Price/Unit</th>
|
||||||
|
<th>Total</th>
|
||||||
|
</tr>
|
||||||
|
{% for line_item in invoice.lineitem_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{line_item.description}}</td>
|
||||||
|
<td>{{line_item.quantity}}</td>
|
||||||
|
<td>{{line_item.price}}</td>
|
||||||
|
<td><!-- multiply --> FIXME</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<th>TOTAL</th>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td>{{ invoice.value }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
28
registrasion/templates/product_category.html
Normal file
28
registrasion/templates/product_category.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<!--- Sample template. Move elsewhere once it's ready to go. -->
|
||||||
|
|
||||||
|
{% extends "site_base.html" %}
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<h1>Product Category: {{ category.name }}</h1>
|
||||||
|
|
||||||
|
<p>{{ category.description }}</p>
|
||||||
|
|
||||||
|
<form method="post" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
{{ voucher_form }}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<input type="submit">
|
||||||
|
|
||||||
|
<table>
|
||||||
|
{{ form }}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<input type="submit">
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -3,6 +3,7 @@ import pytz
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
@ -81,7 +82,18 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
||||||
limit_per_user=10,
|
limit_per_user=10,
|
||||||
order=10,
|
order=10,
|
||||||
)
|
)
|
||||||
cls.PROD_2.save()
|
cls.PROD_3.save()
|
||||||
|
|
||||||
|
cls.PROD_4 = rego.Product.objects.create(
|
||||||
|
name="Product 4",
|
||||||
|
description="This is a test product. It costs $5. "
|
||||||
|
"A user may have 10 of them.",
|
||||||
|
category=cls.CAT_2,
|
||||||
|
price=Decimal("5.00"),
|
||||||
|
limit_per_user=10,
|
||||||
|
order=10,
|
||||||
|
)
|
||||||
|
cls.PROD_4.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_ceiling(cls, name, limit=None, start_time=None, end_time=None):
|
def make_ceiling(cls, name, limit=None, start_time=None, end_time=None):
|
||||||
|
@ -159,6 +171,38 @@ class BasicCartTests(RegistrationCartTestCase):
|
||||||
item = items[0]
|
item = items[0]
|
||||||
self.assertEquals(2, item.quantity)
|
self.assertEquals(2, item.quantity)
|
||||||
|
|
||||||
|
def test_set_quantity(self):
|
||||||
|
current_cart = CartController.for_user(self.USER_1)
|
||||||
|
|
||||||
|
def get_item():
|
||||||
|
return rego.ProductItem.objects.get(
|
||||||
|
cart=current_cart.cart,
|
||||||
|
product=self.PROD_1)
|
||||||
|
|
||||||
|
current_cart.set_quantity(self.PROD_1, 1)
|
||||||
|
self.assertEqual(1, get_item().quantity)
|
||||||
|
|
||||||
|
# Setting the quantity to zero should remove the entry from the cart.
|
||||||
|
current_cart.set_quantity(self.PROD_1, 0)
|
||||||
|
with self.assertRaises(ObjectDoesNotExist):
|
||||||
|
get_item()
|
||||||
|
|
||||||
|
current_cart.set_quantity(self.PROD_1, 9)
|
||||||
|
self.assertEqual(9, get_item().quantity)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
current_cart.set_quantity(self.PROD_1, 11)
|
||||||
|
|
||||||
|
self.assertEqual(9, get_item().quantity)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
current_cart.set_quantity(self.PROD_1, -1)
|
||||||
|
|
||||||
|
self.assertEqual(9, get_item().quantity)
|
||||||
|
|
||||||
|
current_cart.set_quantity(self.PROD_1, 2)
|
||||||
|
self.assertEqual(2, get_item().quantity)
|
||||||
|
|
||||||
def test_add_to_cart_per_user_limit(self):
|
def test_add_to_cart_per_user_limit(self):
|
||||||
current_cart = CartController.for_user(self.USER_1)
|
current_cart = CartController.for_user(self.USER_1)
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,10 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
return discount
|
return discount
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_discount_prod_1_includes_cat_2(cls, amount=Decimal(100)):
|
def add_discount_prod_1_includes_cat_2(
|
||||||
|
cls,
|
||||||
|
amount=Decimal(100),
|
||||||
|
quantity=2):
|
||||||
discount = rego.IncludedProductDiscount.objects.create(
|
discount = rego.IncludedProductDiscount.objects.create(
|
||||||
description="PROD_1 includes CAT_2 " + str(amount) + "%",
|
description="PROD_1 includes CAT_2 " + str(amount) + "%",
|
||||||
)
|
)
|
||||||
|
@ -40,7 +43,7 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
discount=discount,
|
discount=discount,
|
||||||
category=cls.CAT_2,
|
category=cls.CAT_2,
|
||||||
percentage=amount,
|
percentage=amount,
|
||||||
quantity=2
|
quantity=quantity,
|
||||||
).save()
|
).save()
|
||||||
return discount
|
return discount
|
||||||
|
|
||||||
|
@ -169,3 +172,31 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
|
|
||||||
discount_items = list(cart.cart.discountitem_set.all())
|
discount_items = list(cart.cart.discountitem_set.all())
|
||||||
self.assertEqual(2, discount_items[0].quantity)
|
self.assertEqual(2, discount_items[0].quantity)
|
||||||
|
|
||||||
|
def test_category_discount_applies_once_per_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)
|
||||||
|
|
||||||
|
# Add two items from category 2
|
||||||
|
cart.add_to_cart(self.PROD_3, 1)
|
||||||
|
cart.add_to_cart(self.PROD_4, 1)
|
||||||
|
|
||||||
|
discount_items = list(cart.cart.discountitem_set.all())
|
||||||
|
# There is one discount, and it should apply to one item.
|
||||||
|
self.assertEqual(1, len(discount_items))
|
||||||
|
self.assertEqual(1, discount_items[0].quantity)
|
||||||
|
|
||||||
|
def test_category_discount_applies_to_highest_value(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)
|
||||||
|
|
||||||
|
# Add two items from category 2, add the less expensive one first
|
||||||
|
cart.add_to_cart(self.PROD_4, 1)
|
||||||
|
cart.add_to_cart(self.PROD_3, 1)
|
||||||
|
|
||||||
|
discount_items = list(cart.cart.discountitem_set.all())
|
||||||
|
# There is one discount, and it should apply to the more expensive.
|
||||||
|
self.assertEqual(1, len(discount_items))
|
||||||
|
self.assertEqual(self.PROD_3, discount_items[0].product)
|
||||||
|
|
|
@ -91,7 +91,7 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
current_cart = CartController.for_user(self.USER_1)
|
current_cart = CartController.for_user(self.USER_1)
|
||||||
current_cart.apply_voucher(voucher)
|
current_cart.apply_voucher(voucher.code)
|
||||||
|
|
||||||
# Should be able to create an invoice after the product is added
|
# Should be able to create an invoice after the product is added
|
||||||
current_cart.add_to_cart(self.PROD_1, 1)
|
current_cart.add_to_cart(self.PROD_1, 1)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import pytz
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion import models as rego
|
||||||
from registrasion.controllers.cart import CartController
|
from registrasion.controllers.cart import CartController
|
||||||
|
@ -15,10 +16,10 @@ UTC = pytz.timezone('UTC')
|
||||||
class VoucherTestCases(RegistrationCartTestCase):
|
class VoucherTestCases(RegistrationCartTestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def new_voucher(self):
|
def new_voucher(self, code="VOUCHER"):
|
||||||
voucher = rego.Voucher.objects.create(
|
voucher = rego.Voucher.objects.create(
|
||||||
recipient="Voucher recipient",
|
recipient="Voucher recipient",
|
||||||
code="VOUCHER",
|
code=code,
|
||||||
limit=1
|
limit=1
|
||||||
)
|
)
|
||||||
voucher.save()
|
voucher.save()
|
||||||
|
@ -30,18 +31,18 @@ class VoucherTestCases(RegistrationCartTestCase):
|
||||||
self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC))
|
self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC))
|
||||||
|
|
||||||
cart_1 = CartController.for_user(self.USER_1)
|
cart_1 = CartController.for_user(self.USER_1)
|
||||||
cart_1.apply_voucher(voucher)
|
cart_1.apply_voucher(voucher.code)
|
||||||
self.assertIn(voucher, cart_1.cart.vouchers.all())
|
self.assertIn(voucher, cart_1.cart.vouchers.all())
|
||||||
|
|
||||||
# Second user should not be able to apply this voucher (it's exhausted)
|
# Second user should not be able to apply this voucher (it's exhausted)
|
||||||
cart_2 = CartController.for_user(self.USER_2)
|
cart_2 = CartController.for_user(self.USER_2)
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
cart_2.apply_voucher(voucher)
|
cart_2.apply_voucher(voucher.code)
|
||||||
|
|
||||||
# After the reservation duration
|
# After the reservation duration
|
||||||
# user 2 should be able to apply voucher
|
# user 2 should be able to apply voucher
|
||||||
self.add_timedelta(rego.Voucher.RESERVATION_DURATION * 2)
|
self.add_timedelta(rego.Voucher.RESERVATION_DURATION * 2)
|
||||||
cart_2.apply_voucher(voucher)
|
cart_2.apply_voucher(voucher.code)
|
||||||
cart_2.cart.active = False
|
cart_2.cart.active = False
|
||||||
cart_2.cart.save()
|
cart_2.cart.save()
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@ class VoucherTestCases(RegistrationCartTestCase):
|
||||||
# voucher, as user 2 has paid for their cart.
|
# voucher, as user 2 has paid for their cart.
|
||||||
self.add_timedelta(rego.Voucher.RESERVATION_DURATION * 2)
|
self.add_timedelta(rego.Voucher.RESERVATION_DURATION * 2)
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
cart_1.apply_voucher(voucher)
|
cart_1.apply_voucher(voucher.code)
|
||||||
|
|
||||||
def test_voucher_enables_item(self):
|
def test_voucher_enables_item(self):
|
||||||
voucher = self.new_voucher()
|
voucher = self.new_voucher()
|
||||||
|
@ -69,7 +70,7 @@ class VoucherTestCases(RegistrationCartTestCase):
|
||||||
current_cart.add_to_cart(self.PROD_1, 1)
|
current_cart.add_to_cart(self.PROD_1, 1)
|
||||||
|
|
||||||
# Apply the voucher
|
# Apply the voucher
|
||||||
current_cart.apply_voucher(voucher)
|
current_cart.apply_voucher(voucher.code)
|
||||||
current_cart.add_to_cart(self.PROD_1, 1)
|
current_cart.add_to_cart(self.PROD_1, 1)
|
||||||
|
|
||||||
def test_voucher_enables_discount(self):
|
def test_voucher_enables_discount(self):
|
||||||
|
@ -89,6 +90,20 @@ class VoucherTestCases(RegistrationCartTestCase):
|
||||||
|
|
||||||
# Having PROD_1 in place should add a discount
|
# Having PROD_1 in place should add a discount
|
||||||
current_cart = CartController.for_user(self.USER_1)
|
current_cart = CartController.for_user(self.USER_1)
|
||||||
current_cart.apply_voucher(voucher)
|
current_cart.apply_voucher(voucher.code)
|
||||||
current_cart.add_to_cart(self.PROD_1, 1)
|
current_cart.add_to_cart(self.PROD_1, 1)
|
||||||
self.assertEqual(1, len(current_cart.cart.discountitem_set.all()))
|
self.assertEqual(1, len(current_cart.cart.discountitem_set.all()))
|
||||||
|
|
||||||
|
def test_voucher_codes_unique(self):
|
||||||
|
voucher1 = self.new_voucher(code="VOUCHER")
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
voucher2 = self.new_voucher(code="VOUCHER")
|
||||||
|
|
||||||
|
def test_multiple_vouchers_work(self):
|
||||||
|
voucher1 = self.new_voucher(code="VOUCHER1")
|
||||||
|
voucher2 = self.new_voucher(code="VOUCHER2")
|
||||||
|
|
||||||
|
def test_vouchers_case_insensitive(self):
|
||||||
|
voucher = self.new_voucher(code="VOUCHeR")
|
||||||
|
current_cart = CartController.for_user(self.USER_1)
|
||||||
|
current_cart.apply_voucher(voucher.code.lower())
|
||||||
|
|
9
registrasion/urls.py
Normal file
9
registrasion/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.conf.urls import url, patterns
|
||||||
|
|
||||||
|
urlpatterns = patterns(
|
||||||
|
"registrasion.views",
|
||||||
|
url(r"^category/([0-9]+)$", "product_category", name="product_category"),
|
||||||
|
url(r"^checkout$", "checkout", name="checkout"),
|
||||||
|
url(r"^invoice/([0-9]+)$", "invoice", name="invoice"),
|
||||||
|
url(r"^invoice/([0-9]+)/pay$", "pay_invoice", name="pay_invoice"),
|
||||||
|
)
|
135
registrasion/views.py
Normal file
135
registrasion/views.py
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
from registrasion import forms
|
||||||
|
from registrasion import models as rego
|
||||||
|
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
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import transaction
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def product_category(request, category_id):
|
||||||
|
''' Registration selections form for a specific category of items '''
|
||||||
|
|
||||||
|
PRODUCTS_FORM_PREFIX = "products"
|
||||||
|
VOUCHERS_FORM_PREFIX = "vouchers"
|
||||||
|
|
||||||
|
category_id = int(category_id) # Routing is [0-9]+
|
||||||
|
category = rego.Category.objects.get(pk=category_id)
|
||||||
|
current_cart = CartController.for_user(request.user)
|
||||||
|
|
||||||
|
CategoryForm = forms.CategoryForm(category)
|
||||||
|
|
||||||
|
products = rego.Product.objects.filter(category=category)
|
||||||
|
products = products.order_by("order")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
cat_form = CategoryForm(request.POST, request.FILES, prefix=PRODUCTS_FORM_PREFIX)
|
||||||
|
voucher_form = forms.VoucherForm(request.POST, prefix=VOUCHERS_FORM_PREFIX)
|
||||||
|
|
||||||
|
if voucher_form.is_valid():
|
||||||
|
# Apply voucher
|
||||||
|
# leave
|
||||||
|
voucher = voucher_form.cleaned_data["voucher"]
|
||||||
|
try:
|
||||||
|
current_cart.apply_voucher(voucher)
|
||||||
|
except Exception as e:
|
||||||
|
voucher_form.add_error("voucher", e)
|
||||||
|
elif cat_form.is_valid():
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
for product_id, quantity, field_name \
|
||||||
|
in cat_form.product_quantities():
|
||||||
|
product = rego.Product.objects.get(pk=product_id)
|
||||||
|
try:
|
||||||
|
current_cart.set_quantity(
|
||||||
|
product, quantity, batched=True)
|
||||||
|
except ValidationError as ve:
|
||||||
|
cat_form.add_error(field_name, ve)
|
||||||
|
if cat_form.errors:
|
||||||
|
raise ValidationError("Cannot add that stuff")
|
||||||
|
current_cart.end_batch()
|
||||||
|
except ValidationError as ve:
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Create initial data for each of products in category
|
||||||
|
items = rego.ProductItem.objects.filter(
|
||||||
|
product__category=category,
|
||||||
|
cart=current_cart.cart,
|
||||||
|
)
|
||||||
|
quantities = []
|
||||||
|
for product in products:
|
||||||
|
# Only add items that are enabled.
|
||||||
|
prod = ProductController(product)
|
||||||
|
try:
|
||||||
|
quantity = items.get(product=product).quantity
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
quantity = 0
|
||||||
|
quantities.append((product, quantity))
|
||||||
|
|
||||||
|
initial = CategoryForm.initial_data(quantities)
|
||||||
|
cat_form = CategoryForm(prefix=PRODUCTS_FORM_PREFIX, initial=initial)
|
||||||
|
|
||||||
|
voucher_form = forms.VoucherForm(prefix=VOUCHERS_FORM_PREFIX)
|
||||||
|
|
||||||
|
for product in products:
|
||||||
|
# Remove fields that do not have an enabling condition.
|
||||||
|
prod = ProductController(product)
|
||||||
|
if not prod.can_add_with_enabling_conditions(request.user, 0):
|
||||||
|
cat_form.disable_product(product)
|
||||||
|
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"category": category,
|
||||||
|
"form": cat_form,
|
||||||
|
"voucher_form": voucher_form,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "product_category.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def checkout(request):
|
||||||
|
''' Runs checkout for the current cart of items, ideally generating an
|
||||||
|
invoice. '''
|
||||||
|
|
||||||
|
current_cart = CartController.for_user(request.user)
|
||||||
|
current_invoice = InvoiceController.for_cart(current_cart.cart)
|
||||||
|
|
||||||
|
return redirect("invoice", current_invoice.invoice.id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def invoice(request, invoice_id):
|
||||||
|
''' Displays an invoice for a given invoice id. '''
|
||||||
|
|
||||||
|
invoice_id = int(invoice_id)
|
||||||
|
inv = rego.Invoice.objects.get(pk=invoice_id)
|
||||||
|
current_invoice = InvoiceController(inv)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"invoice": current_invoice.invoice,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "invoice.html", data)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def pay_invoice(request, invoice_id):
|
||||||
|
''' Marks the invoice with the given invoice id as paid.
|
||||||
|
WORK IN PROGRESS FUNCTION. Must be replaced with real payment workflow.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
invoice_id = int(invoice_id)
|
||||||
|
inv = rego.Invoice.objects.get(pk=invoice_id)
|
||||||
|
current_invoice = InvoiceController(inv)
|
||||||
|
if not inv.paid and not current_invoice.is_valid():
|
||||||
|
current_invoice.pay("Demo invoice payment", inv.value)
|
||||||
|
|
||||||
|
return redirect("invoice", current_invoice.invoice.id)
|
Loading…
Reference in a new issue