Merge branch 'cart_status_overhaul'

This commit is contained in:
Christopher Neugebauer 2016-04-25 15:37:13 +10:00
commit c135c77d6c
19 changed files with 158 additions and 85 deletions

View file

@ -30,14 +30,16 @@ class CartController(object):
if there isn't one ready yet. '''
try:
existing = commerce.Cart.objects.get(user=user, active=True)
existing = commerce.Cart.objects.get(
user=user,
status=commerce.Cart.STATUS_ACTIVE,
)
except ObjectDoesNotExist:
existing = commerce.Cart.objects.create(
user=user,
time_last_updated=timezone.now(),
reservation_duration=datetime.timedelta(),
)
existing.save()
)
return cls(existing)
def extend_reservation(self):
@ -367,11 +369,10 @@ class CartController(object):
allowed = candidate.quantity
if ours > allowed:
discount_item.quantity = allowed
discount_item.save()
# Update the remaining quantity.
quantity = ours - allowed
else:
quantity = 0
candidate.quantity -= discount_item.quantity
discount_item.save()

View file

@ -46,8 +46,7 @@ class CategoryController(object):
carts = commerce.Cart.objects.filter(
user=user,
active=False,
released=False,
status=commerce.Cart.STATUS_PAID,
)
items = commerce.ProductItem.objects.filter(

View file

@ -201,7 +201,8 @@ class CategoryConditionController(ConditionController):
''' returns True if the user has a product from a category that invokes
this condition in one of their carts '''
carts = commerce.Cart.objects.filter(user=user, released=False)
carts = commerce.Cart.objects.filter(user=user)
carts = carts.exclude(status=commerce.Cart.STATUS_RELEASED)
enabling_products = inventory.Product.objects.filter(
category=self.condition.enabling_category,
)
@ -223,7 +224,8 @@ class ProductConditionController(ConditionController):
''' returns True if the user has a product that invokes this
condition in one of their carts '''
carts = commerce.Cart.objects.filter(user=user, released=False)
carts = commerce.Cart.objects.filter(user=user)
carts = carts.exclude(status=commerce.Cart.STATUS_RELEASED)
products_count = commerce.ProductItem.objects.filter(
cart__in=carts,
product__in=self.condition.enabling_products.all(),
@ -272,7 +274,7 @@ class TimeOrStockLimitConditionController(ConditionController):
reserved_carts = commerce.Cart.reserved_carts()
reserved_carts = reserved_carts.exclude(
user=user,
active=True,
status=commerce.Cart.STATUS_ACTIVE,
)
items = self._items()

View file

@ -71,8 +71,7 @@ def available_discounts(user, categories, products):
# is not available any more.
past_uses = commerce.DiscountItem.objects.filter(
cart__user=user,
cart__active=False, # Only past carts count
cart__released=False, # You can reuse refunded discounts
cart__status=commerce.Cart.STATUS_PAID, # Only past carts count
discount=real_discount,
)
agg = past_uses.aggregate(Sum("quantity"))

View file

@ -227,7 +227,7 @@ class InvoiceController(ForId, object):
necessary. '''
cart = self.invoice.cart
if cart:
cart.active = False
cart.status = commerce.Cart.STATUS_PAID
cart.save()
self.invoice.status = commerce.Invoice.STATUS_PAID
self.invoice.save()
@ -237,8 +237,7 @@ class InvoiceController(ForId, object):
necessary. '''
cart = self.invoice.cart
if cart:
cart.active = False
cart.released = True
cart.status = commerce.Cart.STATUS_RELEASED
cart.save()
self.invoice.status = commerce.Invoice.STATUS_REFUNDED
self.invoice.save()

View file

@ -68,8 +68,7 @@ class ProductController(object):
carts = commerce.Cart.objects.filter(
user=user,
active=False,
released=False,
status=commerce.Cart.STATUS_PAID,
)
items = commerce.ProductItem.objects.filter(

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-04-25 04:09
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0022_merge'),
]
operations = [
migrations.AlterIndexTogether(
name='cart',
index_together=set([]),
),
]

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-04-25 04:10
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0023_auto_20160425_0409'),
]
operations = [
migrations.AddField(
model_name='cart',
name='status',
field=models.IntegerField(choices=[(1, 'Active'), (2, 'Paid'), (3, 'Released')], db_index=True, default=1),
preserve_default=False,
),
migrations.RemoveField(
model_name='cart',
name='active',
),
migrations.RemoveField(
model_name='cart',
name='released',
),
migrations.AlterIndexTogether(
name='cart',
index_together=set([('status', 'user'), ('status', 'time_last_updated')]),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-04-25 04:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0024_auto_20160425_0410'),
]
operations = [
migrations.AlterField(
model_name='cart',
name='status',
field=models.IntegerField(choices=[(1, 'Active'), (2, 'Paid'), (3, 'Released')], db_index=True, default=1),
),
]

View file

@ -21,15 +21,23 @@ class Cart(models.Model):
class Meta:
app_label = "registrasion"
index_together = [
("active", "time_last_updated"),
("active", "released"),
("active", "user"),
("released", "user"),
("status", "time_last_updated"),
("status", "user"),
]
def __str__(self):
return "%d rev #%d" % (self.id, self.revision)
STATUS_ACTIVE = 1
STATUS_PAID = 2
STATUS_RELEASED = 3
STATUS_TYPES = [
(STATUS_ACTIVE, _("Active")),
(STATUS_PAID, _("Paid")),
(STATUS_RELEASED, _("Released")),
]
user = models.ForeignKey(User)
# ProductItems (foreign key)
vouchers = models.ManyToManyField(inventory.Voucher, blank=True)
@ -38,24 +46,21 @@ class Cart(models.Model):
)
reservation_duration = models.DurationField()
revision = models.PositiveIntegerField(default=1)
active = models.BooleanField(
default=True,
status = models.IntegerField(
choices=STATUS_TYPES,
db_index=True,
default=STATUS_ACTIVE,
)
released = models.BooleanField(
default=False,
db_index=True
) # Refunds etc
@classmethod
def reserved_carts(cls):
''' Gets all carts that are 'reserved' '''
return Cart.objects.filter(
(Q(active=True) &
(Q(status=Cart.STATUS_ACTIVE) &
Q(time_last_updated__gt=(
timezone.now()-F('reservation_duration')
))) |
(Q(active=False) & Q(released=False))
Q(status=Cart.STATUS_PAID)
)

View file

@ -73,7 +73,7 @@ def items_pending(context):
all_items = commerce.ProductItem.objects.filter(
cart__user=context.request.user,
cart__active=True,
cart__status=commerce.Cart.STATUS_ACTIVE,
).select_related(
"product",
"product__category",
@ -100,8 +100,7 @@ def items_purchased(context, category=None):
all_items = commerce.ProductItem.objects.filter(
cart__user=context.request.user,
cart__active=False,
cart__released=False,
cart__status=commerce.Cart.STATUS_PAID,
).select_related("product", "product__category")
if category:

View file

@ -28,8 +28,9 @@ class TestingCartController(CartController):
self.set_quantity(product, old_quantity + quantity)
def next_cart(self):
self.cart.active = False
self.cart.save()
if self.cart.status == commerce.Cart.STATUS_ACTIVE:
self.cart.status = commerce.Cart.STATUS_PAID
self.cart.save()
class TestingInvoiceController(InvoiceController):

View file

@ -5,6 +5,7 @@ from decimal import Decimal
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.core.management import call_command
from django.test import TestCase
from registrasion.models import commerce
@ -24,6 +25,16 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
def setUp(self):
super(RegistrationCartTestCase, self).setUp()
def tearDown(self):
if False:
# If you're seeing segfaults in tests, enable this.
call_command('flush', verbosity=0, interactive=False,
reset_sequences=False,
allow_cascade=False,
inhibit_post_migrate=False)
super(RegistrationCartTestCase, self).tearDown()
@classmethod
def setUpTestData(cls):
@ -40,17 +51,13 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
password='top_secret')
attendee1 = people.Attendee.get_instance(cls.USER_1)
attendee1.save()
profile1 = people.AttendeeProfileBase.objects.create(
attendee=attendee1,
)
profile1.save()
attendee2 = people.Attendee.get_instance(cls.USER_2)
attendee2.save()
profile2 = people.AttendeeProfileBase.objects.create(
attendee=attendee2,
)
profile2.save()
cls.RESERVATION = datetime.timedelta(hours=1)
@ -63,7 +70,6 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
render_type=inventory.Category.RENDER_TYPE_RADIO,
required=False,
)
cat.save()
cls.categories.append(cat)
cls.CAT_1 = cls.categories[0]
@ -80,7 +86,6 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
limit_per_user=10,
order=1,
)
prod.save()
cls.products.append(prod)
cls.PROD_1 = cls.products[0]
@ -91,7 +96,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
cls.PROD_4.price = Decimal("5.00")
cls.PROD_4.save()
# Burn through some carts -- this made some past EC tests fail
# Burn through some carts -- this made some past flag tests fail
current_cart = TestingCartController.for_user(cls.USER_1)
current_cart.next_cart()
@ -109,9 +114,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
start_time=start_time,
end_time=end_time
)
limit_ceiling.save()
limit_ceiling.products.add(cls.PROD_1, cls.PROD_2)
limit_ceiling.save()
@classmethod
def make_category_ceiling(
@ -123,9 +126,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
start_time=start_time,
end_time=end_time
)
limit_ceiling.save()
limit_ceiling.categories.add(cls.CAT_1)
limit_ceiling.save()
@classmethod
def make_discount_ceiling(
@ -137,13 +138,12 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
end_time=end_time,
limit=limit,
)
limit_ceiling.save()
conditions.DiscountForProduct.objects.create(
discount=limit_ceiling,
product=cls.PROD_1,
percentage=percentage,
quantity=10,
).save()
)
@classmethod
def new_voucher(self, code="VOUCHER", limit=1):
@ -152,7 +152,6 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
code=code,
limit=limit,
)
voucher.save()
return voucher
@classmethod

View file

@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError
from controller_helpers import TestingCartController
from test_cart import RegistrationCartTestCase
from registrasion.models import commerce
from registrasion.models import conditions
UTC = pytz.timezone('UTC')
@ -146,8 +147,8 @@ class CeilingsTestCases(RegistrationCartTestCase):
with self.assertRaises(ValidationError):
second_cart.add_to_cart(self.PROD_1, 1)
first_cart.cart.released = True
first_cart.next_cart()
first_cart.cart.status = commerce.Cart.STATUS_RELEASED
first_cart.cart.save()
second_cart.add_to_cart(self.PROD_1, 1)
@ -159,13 +160,12 @@ class CeilingsTestCases(RegistrationCartTestCase):
description="VOUCHER RECIPIENT",
voucher=voucher,
)
discount.save()
conditions.DiscountForProduct.objects.create(
discount=discount,
product=self.PROD_1,
percentage=100,
quantity=1
).save()
)
# Buy two of PROD_1, in separate carts:
cart = TestingCartController.for_user(self.USER_1)
@ -173,7 +173,7 @@ class CeilingsTestCases(RegistrationCartTestCase):
# and not the ceiling discount.
cart.apply_voucher("VOUCHER")
cart.add_to_cart(self.PROD_1, 1)
self.assertEqual(1, len(cart.cart.discountitem_set.all()))
self.assertEqual(1, cart.cart.discountitem_set.count())
cart.next_cart()
@ -181,4 +181,4 @@ class CeilingsTestCases(RegistrationCartTestCase):
# ceiling discount
cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1)
self.assertEqual(1, len(cart.cart.discountitem_set.all()))
self.assertEqual(1, cart.cart.discountitem_set.count())

View file

@ -2,6 +2,7 @@ import pytz
from decimal import Decimal
from registrasion.models import commerce
from registrasion.models import conditions
from registrasion.controllers import discount
from controller_helpers import TestingCartController
@ -22,15 +23,13 @@ class DiscountTestCase(RegistrationCartTestCase):
discount = conditions.IncludedProductDiscount.objects.create(
description="PROD_1 includes PROD_2 " + str(amount) + "%",
)
discount.save()
discount.enabling_products.add(cls.PROD_1)
discount.save()
conditions.DiscountForProduct.objects.create(
discount=discount,
product=cls.PROD_2,
percentage=amount,
quantity=quantity,
).save()
)
return discount
@classmethod
@ -42,15 +41,13 @@ class DiscountTestCase(RegistrationCartTestCase):
discount = conditions.IncludedProductDiscount.objects.create(
description="PROD_1 includes CAT_2 " + str(amount) + "%",
)
discount.save()
discount.enabling_products.add(cls.PROD_1)
discount.save()
conditions.DiscountForCategory.objects.create(
discount=discount,
category=cls.CAT_2,
percentage=amount,
quantity=quantity,
).save()
)
return discount
@classmethod
@ -63,21 +60,19 @@ class DiscountTestCase(RegistrationCartTestCase):
description="PROD_1 includes PROD_3 and PROD_4 " +
str(amount) + "%",
)
discount.save()
discount.enabling_products.add(cls.PROD_1)
discount.save()
conditions.DiscountForProduct.objects.create(
discount=discount,
product=cls.PROD_3,
percentage=amount,
quantity=quantity,
).save()
)
conditions.DiscountForProduct.objects.create(
discount=discount,
product=cls.PROD_4,
percentage=amount,
quantity=quantity,
).save()
)
return discount
def test_discount_is_applied(self):
@ -386,7 +381,6 @@ class DiscountTestCase(RegistrationCartTestCase):
)
self.assertEqual(1, len(discounts))
cart.cart.active = False # Keep discount enabled
cart.next_cart()
cart = TestingCartController.for_user(self.USER_1)
@ -401,8 +395,8 @@ class DiscountTestCase(RegistrationCartTestCase):
)
self.assertEqual(0, len(discounts))
cart.cart.released = True
cart.next_cart()
cart.cart.status = commerce.Cart.STATUS_RELEASED
cart.cart.save()
discounts = discount.available_discounts(
self.USER_1,

View file

@ -23,10 +23,8 @@ class FlagTestCases(RegistrationCartTestCase):
description="Product condition",
condition=condition,
)
flag.save()
flag.products.add(cls.PROD_1)
flag.enabling_products.add(cls.PROD_2)
flag.save()
@classmethod
def add_product_flag_on_category(
@ -39,10 +37,8 @@ class FlagTestCases(RegistrationCartTestCase):
description="Product condition",
condition=condition,
)
flag.save()
flag.categories.add(cls.CAT_1)
flag.enabling_products.add(cls.PROD_3)
flag.save()
def add_category_flag(cls, condition=conditions.FlagBase.ENABLE_IF_TRUE):
''' Adds a category flag condition: adding PROD_1 to a cart is
@ -52,9 +48,7 @@ class FlagTestCases(RegistrationCartTestCase):
condition=condition,
enabling_category=cls.CAT_2,
)
flag.save()
flag.products.add(cls.PROD_1)
flag.save()
def test_product_flag_enables_product(self):
self.add_product_flag()
@ -265,8 +259,8 @@ class FlagTestCases(RegistrationCartTestCase):
cart_2.add_to_cart(self.PROD_1, 1)
cart_2.set_quantity(self.PROD_1, 0)
cart.cart.released = True
cart.next_cart()
cart.cart.status = commerce.Cart.STATUS_RELEASED
cart.cart.save()
with self.assertRaises(ValidationError):
cart_2.set_quantity(self.PROD_1, 1)
@ -283,8 +277,8 @@ class FlagTestCases(RegistrationCartTestCase):
cart_2.add_to_cart(self.PROD_1, 1)
cart_2.set_quantity(self.PROD_1, 0)
cart.cart.released = True
cart.next_cart()
cart.cart.status = commerce.Cart.STATUS_RELEASED
cart.cart.save()
with self.assertRaises(ValidationError):
cart_2.set_quantity(self.PROD_1, 1)

View file

@ -97,7 +97,10 @@ class InvoiceTestCase(RegistrationCartTestCase):
self.assertTrue(invoice.invoice.is_paid)
# Cart should not be active
self.assertFalse(invoice.invoice.cart.active)
self.assertNotEqual(
commerce.Cart.STATUS_ACTIVE,
invoice.invoice.cart.status,
)
# Asking for a cart should generate a new one
new_cart = TestingCartController.for_user(self.USER_1)
@ -482,7 +485,7 @@ class InvoiceTestCase(RegistrationCartTestCase):
notes = commerce.CreditNote.objects.filter(invoice=invoice.invoice)
notes = sorted(notes, key = lambda note: note.value)
self.assertEqual(cnval, notes[0].value)
self.assertEqual(val, notes[1].value)

View file

@ -5,6 +5,8 @@ from controller_helpers import TestingInvoiceController
from test_cart import RegistrationCartTestCase
from registrasion.models import commerce
UTC = pytz.timezone('UTC')
@ -21,10 +23,16 @@ class RefundTestCase(RegistrationCartTestCase):
self.assertFalse(invoice.invoice.is_void)
self.assertTrue(invoice.invoice.is_paid)
self.assertFalse(invoice.invoice.is_refunded)
self.assertFalse(invoice.invoice.cart.released)
self.assertNotEqual(
commerce.Cart.STATUS_RELEASED,
invoice.invoice.cart.status,
)
invoice.refund()
self.assertFalse(invoice.invoice.is_void)
self.assertFalse(invoice.invoice.is_paid)
self.assertTrue(invoice.invoice.is_refunded)
self.assertTrue(invoice.invoice.cart.released)
self.assertEqual(
commerce.Cart.STATUS_RELEASED,
invoice.invoice.cart.status,
)

View file

@ -4,6 +4,7 @@ import pytz
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.db import transaction
from registrasion.models import conditions
from registrasion.models import inventory
@ -64,9 +65,7 @@ class VoucherTestCases(RegistrationCartTestCase):
voucher=voucher,
condition=conditions.FlagBase.ENABLE_IF_TRUE,
)
flag.save()
flag.products.add(self.PROD_1)
flag.save()
# Adding the product without a voucher will not work
current_cart = TestingCartController.for_user(self.USER_1)
@ -84,13 +83,12 @@ class VoucherTestCases(RegistrationCartTestCase):
description="VOUCHER RECIPIENT",
voucher=voucher,
)
discount.save()
conditions.DiscountForProduct.objects.create(
discount=discount,
product=self.PROD_1,
percentage=Decimal(100),
quantity=1
).save()
)
# Having PROD_1 in place should add a discount
current_cart = TestingCartController.for_user(self.USER_1)
@ -98,6 +96,7 @@ class VoucherTestCases(RegistrationCartTestCase):
current_cart.add_to_cart(self.PROD_1, 1)
self.assertEqual(1, len(current_cart.cart.discountitem_set.all()))
@transaction.atomic
def test_voucher_codes_unique(self):
self.new_voucher(code="VOUCHER")
with self.assertRaises(IntegrityError):