Consolidates models.py into a directory module.
This commit is contained in:
parent
278ca23c29
commit
875f736d67
24 changed files with 1193 additions and 1040 deletions
|
@ -4,7 +4,8 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
import nested_admin
|
import nested_admin
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import conditions
|
||||||
|
from registrasion.models import inventory
|
||||||
|
|
||||||
|
|
||||||
class EffectsDisplayMixin(object):
|
class EffectsDisplayMixin(object):
|
||||||
|
@ -15,12 +16,12 @@ class EffectsDisplayMixin(object):
|
||||||
|
|
||||||
|
|
||||||
class ProductInline(admin.TabularInline):
|
class ProductInline(admin.TabularInline):
|
||||||
model = rego.Product
|
model = inventory.Product
|
||||||
|
|
||||||
|
|
||||||
@admin.register(rego.Category)
|
@admin.register(inventory.Category)
|
||||||
class CategoryAdmin(admin.ModelAdmin):
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
model = rego.Category
|
model = inventory.Category
|
||||||
fields = ("name", "description", "required", "render_type",
|
fields = ("name", "description", "required", "render_type",
|
||||||
"limit_per_user", "order",)
|
"limit_per_user", "order",)
|
||||||
list_display = ("name", "description")
|
list_display = ("name", "description")
|
||||||
|
@ -29,9 +30,9 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(rego.Product)
|
@admin.register(inventory.Product)
|
||||||
class ProductAdmin(admin.ModelAdmin):
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
model = rego.Product
|
model = inventory.Product
|
||||||
list_display = ("name", "category", "description")
|
list_display = ("name", "category", "description")
|
||||||
list_filter = ("category", )
|
list_filter = ("category", )
|
||||||
|
|
||||||
|
@ -39,18 +40,18 @@ class ProductAdmin(admin.ModelAdmin):
|
||||||
# Discounts
|
# Discounts
|
||||||
|
|
||||||
class DiscountForProductInline(admin.TabularInline):
|
class DiscountForProductInline(admin.TabularInline):
|
||||||
model = rego.DiscountForProduct
|
model = conditions.DiscountForProduct
|
||||||
verbose_name = _("Product included in discount")
|
verbose_name = _("Product included in discount")
|
||||||
verbose_name_plural = _("Products included in discount")
|
verbose_name_plural = _("Products included in discount")
|
||||||
|
|
||||||
|
|
||||||
class DiscountForCategoryInline(admin.TabularInline):
|
class DiscountForCategoryInline(admin.TabularInline):
|
||||||
model = rego.DiscountForCategory
|
model = conditions.DiscountForCategory
|
||||||
verbose_name = _("Category included in discount")
|
verbose_name = _("Category included in discount")
|
||||||
verbose_name_plural = _("Categories included in discount")
|
verbose_name_plural = _("Categories included in discount")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(rego.TimeOrStockLimitDiscount)
|
@admin.register(conditions.TimeOrStockLimitDiscount)
|
||||||
class TimeOrStockLimitDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
|
class TimeOrStockLimitDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
|
||||||
list_display = (
|
list_display = (
|
||||||
"description",
|
"description",
|
||||||
|
@ -67,7 +68,7 @@ class TimeOrStockLimitDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(rego.IncludedProductDiscount)
|
@admin.register(conditions.IncludedProductDiscount)
|
||||||
class IncludedProductDiscountAdmin(admin.ModelAdmin):
|
class IncludedProductDiscountAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
def enablers(self, obj):
|
def enablers(self, obj):
|
||||||
|
@ -87,7 +88,7 @@ class IncludedProductDiscountAdmin(admin.ModelAdmin):
|
||||||
# Vouchers
|
# Vouchers
|
||||||
|
|
||||||
class VoucherDiscountInline(nested_admin.NestedStackedInline):
|
class VoucherDiscountInline(nested_admin.NestedStackedInline):
|
||||||
model = rego.VoucherDiscount
|
model = conditions.VoucherDiscount
|
||||||
verbose_name = _("Discount")
|
verbose_name = _("Discount")
|
||||||
|
|
||||||
# TODO work out why we're allowed to add more than one?
|
# TODO work out why we're allowed to add more than one?
|
||||||
|
@ -100,7 +101,7 @@ class VoucherDiscountInline(nested_admin.NestedStackedInline):
|
||||||
|
|
||||||
|
|
||||||
class VoucherFlagInline(nested_admin.NestedStackedInline):
|
class VoucherFlagInline(nested_admin.NestedStackedInline):
|
||||||
model = rego.VoucherFlag
|
model = conditions.VoucherFlag
|
||||||
verbose_name = _("Product and category enabled by voucher")
|
verbose_name = _("Product and category enabled by voucher")
|
||||||
verbose_name_plural = _("Products and categories enabled by voucher")
|
verbose_name_plural = _("Products and categories enabled by voucher")
|
||||||
|
|
||||||
|
@ -109,7 +110,7 @@ class VoucherFlagInline(nested_admin.NestedStackedInline):
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
|
|
||||||
@admin.register(rego.Voucher)
|
@admin.register(inventory.Voucher)
|
||||||
class VoucherAdmin(nested_admin.NestedAdmin):
|
class VoucherAdmin(nested_admin.NestedAdmin):
|
||||||
|
|
||||||
def effects(self, obj):
|
def effects(self, obj):
|
||||||
|
@ -133,7 +134,7 @@ class VoucherAdmin(nested_admin.NestedAdmin):
|
||||||
|
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
model = rego.Voucher
|
model = inventory.Voucher
|
||||||
list_display = ("recipient", "code", "effects")
|
list_display = ("recipient", "code", "effects")
|
||||||
inlines = [
|
inlines = [
|
||||||
VoucherDiscountInline,
|
VoucherDiscountInline,
|
||||||
|
@ -142,7 +143,7 @@ class VoucherAdmin(nested_admin.NestedAdmin):
|
||||||
|
|
||||||
|
|
||||||
# Enabling conditions
|
# Enabling conditions
|
||||||
@admin.register(rego.ProductFlag)
|
@admin.register(conditions.ProductFlag)
|
||||||
class ProductFlagAdmin(
|
class ProductFlagAdmin(
|
||||||
nested_admin.NestedAdmin,
|
nested_admin.NestedAdmin,
|
||||||
EffectsDisplayMixin):
|
EffectsDisplayMixin):
|
||||||
|
@ -150,7 +151,7 @@ class ProductFlagAdmin(
|
||||||
def enablers(self, obj):
|
def enablers(self, obj):
|
||||||
return list(obj.enabling_products.all())
|
return list(obj.enabling_products.all())
|
||||||
|
|
||||||
model = rego.ProductFlag
|
model = conditions.ProductFlag
|
||||||
fields = ("description", "enabling_products", "condition", "products",
|
fields = ("description", "enabling_products", "condition", "products",
|
||||||
"categories"),
|
"categories"),
|
||||||
|
|
||||||
|
@ -158,12 +159,12 @@ class ProductFlagAdmin(
|
||||||
|
|
||||||
|
|
||||||
# Enabling conditions
|
# Enabling conditions
|
||||||
@admin.register(rego.CategoryFlag)
|
@admin.register(conditions.CategoryFlag)
|
||||||
class CategoryFlagAdmin(
|
class CategoryFlagAdmin(
|
||||||
nested_admin.NestedAdmin,
|
nested_admin.NestedAdmin,
|
||||||
EffectsDisplayMixin):
|
EffectsDisplayMixin):
|
||||||
|
|
||||||
model = rego.CategoryFlag
|
model = conditions.CategoryFlag
|
||||||
fields = ("description", "enabling_category", "condition", "products",
|
fields = ("description", "enabling_category", "condition", "products",
|
||||||
"categories"),
|
"categories"),
|
||||||
|
|
||||||
|
@ -172,11 +173,11 @@ class CategoryFlagAdmin(
|
||||||
|
|
||||||
|
|
||||||
# Enabling conditions
|
# Enabling conditions
|
||||||
@admin.register(rego.TimeOrStockLimitFlag)
|
@admin.register(conditions.TimeOrStockLimitFlag)
|
||||||
class TimeOrStockLimitFlagAdmin(
|
class TimeOrStockLimitFlagAdmin(
|
||||||
nested_admin.NestedAdmin,
|
nested_admin.NestedAdmin,
|
||||||
EffectsDisplayMixin):
|
EffectsDisplayMixin):
|
||||||
model = rego.TimeOrStockLimitFlag
|
model = conditions.TimeOrStockLimitFlag
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"description",
|
"description",
|
||||||
|
|
|
@ -9,8 +9,10 @@ from django.db import transaction
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from registrasion import models as rego
|
|
||||||
from registrasion.exceptions import CartValidationError
|
from registrasion.exceptions import CartValidationError
|
||||||
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import conditions
|
||||||
|
from registrasion.models import inventory
|
||||||
|
|
||||||
from category import CategoryController
|
from category import CategoryController
|
||||||
from conditions import ConditionController
|
from conditions import ConditionController
|
||||||
|
@ -28,9 +30,9 @@ class CartController(object):
|
||||||
if there isn't one ready yet. '''
|
if there isn't one ready yet. '''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
existing = rego.Cart.objects.get(user=user, active=True)
|
existing = commerce.Cart.objects.get(user=user, active=True)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
existing = rego.Cart.objects.create(
|
existing = commerce.Cart.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
time_last_updated=timezone.now(),
|
time_last_updated=timezone.now(),
|
||||||
reservation_duration=datetime.timedelta(),
|
reservation_duration=datetime.timedelta(),
|
||||||
|
@ -47,10 +49,10 @@ class CartController(object):
|
||||||
|
|
||||||
# If we have vouchers, we're entitled to an hour at minimum.
|
# If we have vouchers, we're entitled to an hour at minimum.
|
||||||
if len(self.cart.vouchers.all()) >= 1:
|
if len(self.cart.vouchers.all()) >= 1:
|
||||||
reservations.append(rego.Voucher.RESERVATION_DURATION)
|
reservations.append(inventory.Voucher.RESERVATION_DURATION)
|
||||||
|
|
||||||
# Else, it's the maximum of the included products
|
# Else, it's the maximum of the included products
|
||||||
items = rego.ProductItem.objects.filter(cart=self.cart)
|
items = commerce.ProductItem.objects.filter(cart=self.cart)
|
||||||
agg = items.aggregate(Max("product__reservation_duration"))
|
agg = items.aggregate(Max("product__reservation_duration"))
|
||||||
product_max = agg["product__reservation_duration__max"]
|
product_max = agg["product__reservation_duration__max"]
|
||||||
|
|
||||||
|
@ -79,7 +81,7 @@ class CartController(object):
|
||||||
is violated. `product_quantities` is an iterable of (product, quantity)
|
is violated. `product_quantities` is an iterable of (product, quantity)
|
||||||
pairs. '''
|
pairs. '''
|
||||||
|
|
||||||
items_in_cart = rego.ProductItem.objects.filter(cart=self.cart)
|
items_in_cart = commerce.ProductItem.objects.filter(cart=self.cart)
|
||||||
items_in_cart = items_in_cart.select_related(
|
items_in_cart = items_in_cart.select_related(
|
||||||
"product",
|
"product",
|
||||||
"product__category",
|
"product__category",
|
||||||
|
@ -99,14 +101,14 @@ class CartController(object):
|
||||||
|
|
||||||
for product, quantity in product_quantities:
|
for product, quantity in product_quantities:
|
||||||
try:
|
try:
|
||||||
product_item = rego.ProductItem.objects.get(
|
product_item = commerce.ProductItem.objects.get(
|
||||||
cart=self.cart,
|
cart=self.cart,
|
||||||
product=product,
|
product=product,
|
||||||
)
|
)
|
||||||
product_item.quantity = quantity
|
product_item.quantity = quantity
|
||||||
product_item.save()
|
product_item.save()
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
rego.ProductItem.objects.create(
|
commerce.ProductItem.objects.create(
|
||||||
cart=self.cart,
|
cart=self.cart,
|
||||||
product=product,
|
product=product,
|
||||||
quantity=quantity,
|
quantity=quantity,
|
||||||
|
@ -176,7 +178,7 @@ class CartController(object):
|
||||||
''' Applies the voucher with the given code to this cart. '''
|
''' Applies the voucher with the given code to this cart. '''
|
||||||
|
|
||||||
# Try and find the voucher
|
# Try and find the voucher
|
||||||
voucher = rego.Voucher.objects.get(code=voucher_code.upper())
|
voucher = inventory.Voucher.objects.get(code=voucher_code.upper())
|
||||||
|
|
||||||
# Re-applying vouchers should be idempotent
|
# Re-applying vouchers should be idempotent
|
||||||
if voucher in self.cart.vouchers.all():
|
if voucher in self.cart.vouchers.all():
|
||||||
|
@ -193,7 +195,7 @@ class CartController(object):
|
||||||
Raises ValidationError if not. '''
|
Raises ValidationError if not. '''
|
||||||
|
|
||||||
# Is voucher exhausted?
|
# Is voucher exhausted?
|
||||||
active_carts = rego.Cart.reserved_carts()
|
active_carts = commerce.Cart.reserved_carts()
|
||||||
|
|
||||||
# It's invalid for a user to enter a voucher that's exhausted
|
# It's invalid for a user to enter a voucher that's exhausted
|
||||||
carts_with_voucher = active_carts.filter(vouchers=voucher)
|
carts_with_voucher = active_carts.filter(vouchers=voucher)
|
||||||
|
@ -238,7 +240,7 @@ class CartController(object):
|
||||||
except ValidationError as ve:
|
except ValidationError as ve:
|
||||||
errors.append(ve)
|
errors.append(ve)
|
||||||
|
|
||||||
items = rego.ProductItem.objects.filter(cart=cart)
|
items = commerce.ProductItem.objects.filter(cart=cart)
|
||||||
|
|
||||||
product_quantities = list((i.product, i.quantity) for i in items)
|
product_quantities = list((i.product, i.quantity) for i in items)
|
||||||
try:
|
try:
|
||||||
|
@ -248,7 +250,7 @@ class CartController(object):
|
||||||
errors.append(error.message[1])
|
errors.append(error.message[1])
|
||||||
|
|
||||||
# Validate the discounts
|
# Validate the discounts
|
||||||
discount_items = rego.DiscountItem.objects.filter(cart=cart)
|
discount_items = commerce.DiscountItem.objects.filter(cart=cart)
|
||||||
seen_discounts = set()
|
seen_discounts = set()
|
||||||
|
|
||||||
for discount_item in discount_items:
|
for discount_item in discount_items:
|
||||||
|
@ -256,7 +258,7 @@ class CartController(object):
|
||||||
if discount in seen_discounts:
|
if discount in seen_discounts:
|
||||||
continue
|
continue
|
||||||
seen_discounts.add(discount)
|
seen_discounts.add(discount)
|
||||||
real_discount = rego.DiscountBase.objects.get_subclass(
|
real_discount = conditions.DiscountBase.objects.get_subclass(
|
||||||
pk=discount.pk)
|
pk=discount.pk)
|
||||||
cond = ConditionController.for_condition(real_discount)
|
cond = ConditionController.for_condition(real_discount)
|
||||||
|
|
||||||
|
@ -287,7 +289,7 @@ class CartController(object):
|
||||||
self.cart.vouchers.remove(voucher)
|
self.cart.vouchers.remove(voucher)
|
||||||
|
|
||||||
# Fix products and discounts
|
# Fix products and discounts
|
||||||
items = rego.ProductItem.objects.filter(cart=self.cart)
|
items = commerce.ProductItem.objects.filter(cart=self.cart)
|
||||||
items = items.select_related("product")
|
items = items.select_related("product")
|
||||||
products = set(i.product for i in items)
|
products = set(i.product for i in items)
|
||||||
available = set(ProductController.available_products(
|
available = set(ProductController.available_products(
|
||||||
|
@ -306,7 +308,7 @@ class CartController(object):
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Delete the existing entries.
|
# Delete the existing entries.
|
||||||
rego.DiscountItem.objects.filter(cart=self.cart).delete()
|
commerce.DiscountItem.objects.filter(cart=self.cart).delete()
|
||||||
|
|
||||||
product_items = self.cart.productitem_set.all().select_related(
|
product_items = self.cart.productitem_set.all().select_related(
|
||||||
"product", "product__category",
|
"product", "product__category",
|
||||||
|
@ -331,7 +333,7 @@ class CartController(object):
|
||||||
def matches(discount):
|
def matches(discount):
|
||||||
''' Returns True if and only if the given discount apples to
|
''' Returns True if and only if the given discount apples to
|
||||||
our product. '''
|
our product. '''
|
||||||
if isinstance(discount.clause, rego.DiscountForCategory):
|
if isinstance(discount.clause, conditions.DiscountForCategory):
|
||||||
return discount.clause.category == product.category
|
return discount.clause.category == product.category
|
||||||
else:
|
else:
|
||||||
return discount.clause.product == product
|
return discount.clause.product == product
|
||||||
|
@ -356,7 +358,7 @@ class CartController(object):
|
||||||
|
|
||||||
# Get a provisional instance for this DiscountItem
|
# Get a provisional instance for this DiscountItem
|
||||||
# with the quantity set to as much as we have in the cart
|
# with the quantity set to as much as we have in the cart
|
||||||
discount_item = rego.DiscountItem.objects.create(
|
discount_item = commerce.DiscountItem.objects.create(
|
||||||
product=product,
|
product=product,
|
||||||
cart=self.cart,
|
cart=self.cart,
|
||||||
discount=candidate.discount,
|
discount=candidate.discount,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import inventory
|
||||||
|
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
@ -22,7 +23,9 @@ class CategoryController(object):
|
||||||
from product import ProductController
|
from product import ProductController
|
||||||
|
|
||||||
if products is AllProducts:
|
if products is AllProducts:
|
||||||
products = rego.Product.objects.all().select_related("category")
|
products = inventory.Product.objects.all().select_related(
|
||||||
|
"category",
|
||||||
|
)
|
||||||
|
|
||||||
available = ProductController.available_products(
|
available = ProductController.available_products(
|
||||||
user,
|
user,
|
||||||
|
@ -41,13 +44,13 @@ class CategoryController(object):
|
||||||
# We don't need to waste the following queries
|
# We don't need to waste the following queries
|
||||||
return 99999999
|
return 99999999
|
||||||
|
|
||||||
carts = rego.Cart.objects.filter(
|
carts = commerce.Cart.objects.filter(
|
||||||
user=user,
|
user=user,
|
||||||
active=False,
|
active=False,
|
||||||
released=False,
|
released=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
items = rego.ProductItem.objects.filter(
|
items = commerce.ProductItem.objects.filter(
|
||||||
cart__in=carts,
|
cart__in=carts,
|
||||||
product__category=self.category,
|
product__category=self.category,
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,9 @@ from collections import namedtuple
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import conditions
|
||||||
|
from registrasion.models import inventory
|
||||||
|
|
||||||
|
|
||||||
ConditionAndRemainder = namedtuple(
|
ConditionAndRemainder = namedtuple(
|
||||||
|
@ -29,15 +31,15 @@ class ConditionController(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def for_condition(condition):
|
def for_condition(condition):
|
||||||
CONTROLLERS = {
|
CONTROLLERS = {
|
||||||
rego.CategoryFlag: CategoryConditionController,
|
conditions.CategoryFlag: CategoryConditionController,
|
||||||
rego.IncludedProductDiscount: ProductConditionController,
|
conditions.IncludedProductDiscount: ProductConditionController,
|
||||||
rego.ProductFlag: ProductConditionController,
|
conditions.ProductFlag: ProductConditionController,
|
||||||
rego.TimeOrStockLimitDiscount:
|
conditions.TimeOrStockLimitDiscount:
|
||||||
TimeOrStockLimitDiscountController,
|
TimeOrStockLimitDiscountController,
|
||||||
rego.TimeOrStockLimitFlag:
|
conditions.TimeOrStockLimitFlag:
|
||||||
TimeOrStockLimitFlagController,
|
TimeOrStockLimitFlagController,
|
||||||
rego.VoucherDiscount: VoucherConditionController,
|
conditions.VoucherDiscount: VoucherConditionController,
|
||||||
rego.VoucherFlag: VoucherConditionController,
|
conditions.VoucherFlag: VoucherConditionController,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -121,7 +123,7 @@ class ConditionController(object):
|
||||||
# Get all products covered by this condition, and the products
|
# Get all products covered by this condition, and the products
|
||||||
# from the categories covered by this condition
|
# from the categories covered by this condition
|
||||||
cond_products = condition.products.all()
|
cond_products = condition.products.all()
|
||||||
from_category = rego.Product.objects.filter(
|
from_category = inventory.Product.objects.filter(
|
||||||
category__in=condition.categories.all(),
|
category__in=condition.categories.all(),
|
||||||
).all()
|
).all()
|
||||||
all_products = cond_products | from_category
|
all_products = cond_products | from_category
|
||||||
|
@ -199,11 +201,11 @@ class CategoryConditionController(ConditionController):
|
||||||
''' returns True if the user has a product from a category that invokes
|
''' returns True if the user has a product from a category that invokes
|
||||||
this condition in one of their carts '''
|
this condition in one of their carts '''
|
||||||
|
|
||||||
carts = rego.Cart.objects.filter(user=user, released=False)
|
carts = commerce.Cart.objects.filter(user=user, released=False)
|
||||||
enabling_products = rego.Product.objects.filter(
|
enabling_products = inventory.Product.objects.filter(
|
||||||
category=self.condition.enabling_category,
|
category=self.condition.enabling_category,
|
||||||
)
|
)
|
||||||
products_count = rego.ProductItem.objects.filter(
|
products_count = commerce.ProductItem.objects.filter(
|
||||||
cart__in=carts,
|
cart__in=carts,
|
||||||
product__in=enabling_products,
|
product__in=enabling_products,
|
||||||
).count()
|
).count()
|
||||||
|
@ -221,8 +223,8 @@ class ProductConditionController(ConditionController):
|
||||||
''' returns True if the user has a product that invokes this
|
''' returns True if the user has a product that invokes this
|
||||||
condition in one of their carts '''
|
condition in one of their carts '''
|
||||||
|
|
||||||
carts = rego.Cart.objects.filter(user=user, released=False)
|
carts = commerce.Cart.objects.filter(user=user, released=False)
|
||||||
products_count = rego.ProductItem.objects.filter(
|
products_count = commerce.ProductItem.objects.filter(
|
||||||
cart__in=carts,
|
cart__in=carts,
|
||||||
product__in=self.condition.enabling_products.all(),
|
product__in=self.condition.enabling_products.all(),
|
||||||
).count()
|
).count()
|
||||||
|
@ -267,7 +269,7 @@ class TimeOrStockLimitConditionController(ConditionController):
|
||||||
return 99999999
|
return 99999999
|
||||||
|
|
||||||
# We care about all reserved carts, but not the user's current cart
|
# We care about all reserved carts, but not the user's current cart
|
||||||
reserved_carts = rego.Cart.reserved_carts()
|
reserved_carts = commerce.Cart.reserved_carts()
|
||||||
reserved_carts = reserved_carts.exclude(
|
reserved_carts = reserved_carts.exclude(
|
||||||
user=user,
|
user=user,
|
||||||
active=True,
|
active=True,
|
||||||
|
@ -284,12 +286,12 @@ class TimeOrStockLimitFlagController(
|
||||||
TimeOrStockLimitConditionController):
|
TimeOrStockLimitConditionController):
|
||||||
|
|
||||||
def _items(self):
|
def _items(self):
|
||||||
category_products = rego.Product.objects.filter(
|
category_products = inventory.Product.objects.filter(
|
||||||
category__in=self.ceiling.categories.all(),
|
category__in=self.ceiling.categories.all(),
|
||||||
)
|
)
|
||||||
products = self.ceiling.products.all() | category_products
|
products = self.ceiling.products.all() | category_products
|
||||||
|
|
||||||
product_items = rego.ProductItem.objects.filter(
|
product_items = commerce.ProductItem.objects.filter(
|
||||||
product__in=products.all(),
|
product__in=products.all(),
|
||||||
)
|
)
|
||||||
return product_items
|
return product_items
|
||||||
|
@ -298,7 +300,7 @@ class TimeOrStockLimitFlagController(
|
||||||
class TimeOrStockLimitDiscountController(TimeOrStockLimitConditionController):
|
class TimeOrStockLimitDiscountController(TimeOrStockLimitConditionController):
|
||||||
|
|
||||||
def _items(self):
|
def _items(self):
|
||||||
discount_items = rego.DiscountItem.objects.filter(
|
discount_items = commerce.DiscountItem.objects.filter(
|
||||||
discount=self.ceiling,
|
discount=self.ceiling,
|
||||||
)
|
)
|
||||||
return discount_items
|
return discount_items
|
||||||
|
@ -312,7 +314,7 @@ class VoucherConditionController(ConditionController):
|
||||||
|
|
||||||
def is_met(self, user):
|
def is_met(self, user):
|
||||||
''' returns True if the user has the given voucher attached. '''
|
''' returns True if the user has the given voucher attached. '''
|
||||||
carts_count = rego.Cart.objects.filter(
|
carts_count = commerce.Cart.objects.filter(
|
||||||
user=user,
|
user=user,
|
||||||
vouchers=self.condition.voucher,
|
vouchers=self.condition.voucher,
|
||||||
).count()
|
).count()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
|
||||||
|
|
||||||
class CreditNoteController(object):
|
class CreditNoteController(object):
|
||||||
|
@ -14,7 +14,7 @@ class CreditNoteController(object):
|
||||||
the given invoice. You need to call InvoiceController.update_status()
|
the given invoice. You need to call InvoiceController.update_status()
|
||||||
to set the status correctly, if appropriate. '''
|
to set the status correctly, if appropriate. '''
|
||||||
|
|
||||||
credit_note = rego.CreditNote.objects.create(
|
credit_note = commerce.CreditNote.objects.create(
|
||||||
invoice=invoice,
|
invoice=invoice,
|
||||||
amount=0-value, # Credit notes start off as a payment against inv.
|
amount=0-value, # Credit notes start off as a payment against inv.
|
||||||
reference="ONE MOMENT",
|
reference="ONE MOMENT",
|
||||||
|
@ -39,7 +39,7 @@ class CreditNoteController(object):
|
||||||
inv.validate_allowed_to_pay()
|
inv.validate_allowed_to_pay()
|
||||||
|
|
||||||
# Apply payment to invoice
|
# Apply payment to invoice
|
||||||
rego.CreditNoteApplication.objects.create(
|
commerce.CreditNoteApplication.objects.create(
|
||||||
parent=self.credit_note,
|
parent=self.credit_note,
|
||||||
invoice=invoice,
|
invoice=invoice,
|
||||||
amount=self.credit_note.value,
|
amount=self.credit_note.value,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from conditions import ConditionController
|
from conditions import ConditionController
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import conditions
|
||||||
|
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
@ -24,15 +25,15 @@ def available_discounts(user, categories, products):
|
||||||
not including products that are pending purchase. '''
|
not including products that are pending purchase. '''
|
||||||
|
|
||||||
# discounts that match provided categories
|
# discounts that match provided categories
|
||||||
category_discounts = rego.DiscountForCategory.objects.filter(
|
category_discounts = conditions.DiscountForCategory.objects.filter(
|
||||||
category__in=categories
|
category__in=categories
|
||||||
)
|
)
|
||||||
# discounts that match provided products
|
# discounts that match provided products
|
||||||
product_discounts = rego.DiscountForProduct.objects.filter(
|
product_discounts = conditions.DiscountForProduct.objects.filter(
|
||||||
product__in=products
|
product__in=products
|
||||||
)
|
)
|
||||||
# discounts that match categories for provided products
|
# discounts that match categories for provided products
|
||||||
product_category_discounts = rego.DiscountForCategory.objects.filter(
|
product_category_discounts = conditions.DiscountForCategory.objects.filter(
|
||||||
category__in=(product.category for product in products)
|
category__in=(product.category for product in products)
|
||||||
)
|
)
|
||||||
# (Not relevant: discounts that match products in provided categories)
|
# (Not relevant: discounts that match products in provided categories)
|
||||||
|
@ -60,7 +61,7 @@ def available_discounts(user, categories, products):
|
||||||
failed_discounts = set()
|
failed_discounts = set()
|
||||||
|
|
||||||
for discount in potential_discounts:
|
for discount in potential_discounts:
|
||||||
real_discount = rego.DiscountBase.objects.get_subclass(
|
real_discount = conditions.DiscountBase.objects.get_subclass(
|
||||||
pk=discount.discount.pk,
|
pk=discount.discount.pk,
|
||||||
)
|
)
|
||||||
cond = ConditionController.for_condition(real_discount)
|
cond = ConditionController.for_condition(real_discount)
|
||||||
|
@ -68,7 +69,7 @@ def available_discounts(user, categories, products):
|
||||||
# Count the past uses of the given discount item.
|
# Count the past uses of the given discount item.
|
||||||
# If this user has exceeded the limit for the clause, this clause
|
# If this user has exceeded the limit for the clause, this clause
|
||||||
# is not available any more.
|
# is not available any more.
|
||||||
past_uses = rego.DiscountItem.objects.filter(
|
past_uses = commerce.DiscountItem.objects.filter(
|
||||||
cart__user=user,
|
cart__user=user,
|
||||||
cart__active=False, # Only past carts count
|
cart__active=False, # Only past carts count
|
||||||
cart__released=False, # You can reuse refunded discounts
|
cart__released=False, # You can reuse refunded discounts
|
||||||
|
|
|
@ -5,7 +5,9 @@ from django.db import transaction
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import conditions
|
||||||
|
from registrasion.models import people
|
||||||
|
|
||||||
from cart import CartController
|
from cart import CartController
|
||||||
from credit_note import CreditNoteController
|
from credit_note import CreditNoteController
|
||||||
|
@ -25,8 +27,8 @@ class InvoiceController(object):
|
||||||
an invoice is generated.'''
|
an invoice is generated.'''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
invoice = rego.Invoice.objects.exclude(
|
invoice = commerce.Invoice.objects.exclude(
|
||||||
status=rego.Invoice.STATUS_VOID,
|
status=commerce.Invoice.STATUS_VOID,
|
||||||
).get(
|
).get(
|
||||||
cart=cart,
|
cart=cart,
|
||||||
cart_revision=cart.revision,
|
cart_revision=cart.revision,
|
||||||
|
@ -42,19 +44,19 @@ class InvoiceController(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def void_all_invoices(cls, cart):
|
def void_all_invoices(cls, cart):
|
||||||
invoices = rego.Invoice.objects.filter(cart=cart).all()
|
invoices = commerce.Invoice.objects.filter(cart=cart).all()
|
||||||
for invoice in invoices:
|
for invoice in invoices:
|
||||||
cls(invoice).void()
|
cls(invoice).void()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resolve_discount_value(cls, item):
|
def resolve_discount_value(cls, item):
|
||||||
try:
|
try:
|
||||||
condition = rego.DiscountForProduct.objects.get(
|
condition = conditions.DiscountForProduct.objects.get(
|
||||||
discount=item.discount,
|
discount=item.discount,
|
||||||
product=item.product
|
product=item.product
|
||||||
)
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
condition = rego.DiscountForCategory.objects.get(
|
condition = conditions.DiscountForCategory.objects.get(
|
||||||
discount=item.discount,
|
discount=item.discount,
|
||||||
category=item.product.category
|
category=item.product.category
|
||||||
)
|
)
|
||||||
|
@ -75,22 +77,22 @@ class InvoiceController(object):
|
||||||
due = max(issued, reservation_limit)
|
due = max(issued, reservation_limit)
|
||||||
|
|
||||||
# Get the invoice recipient
|
# Get the invoice recipient
|
||||||
profile = rego.AttendeeProfileBase.objects.get_subclass(
|
profile = people.AttendeeProfileBase.objects.get_subclass(
|
||||||
id=cart.user.attendee.attendeeprofilebase.id,
|
id=cart.user.attendee.attendeeprofilebase.id,
|
||||||
)
|
)
|
||||||
recipient = profile.invoice_recipient()
|
recipient = profile.invoice_recipient()
|
||||||
invoice = rego.Invoice.objects.create(
|
invoice = commerce.Invoice.objects.create(
|
||||||
user=cart.user,
|
user=cart.user,
|
||||||
cart=cart,
|
cart=cart,
|
||||||
cart_revision=cart.revision,
|
cart_revision=cart.revision,
|
||||||
status=rego.Invoice.STATUS_UNPAID,
|
status=commerce.Invoice.STATUS_UNPAID,
|
||||||
value=Decimal(),
|
value=Decimal(),
|
||||||
issue_time=issued,
|
issue_time=issued,
|
||||||
due_time=due,
|
due_time=due,
|
||||||
recipient=recipient,
|
recipient=recipient,
|
||||||
)
|
)
|
||||||
|
|
||||||
product_items = rego.ProductItem.objects.filter(cart=cart)
|
product_items = commerce.ProductItem.objects.filter(cart=cart)
|
||||||
|
|
||||||
if len(product_items) == 0:
|
if len(product_items) == 0:
|
||||||
raise ValidationError("Your cart is empty.")
|
raise ValidationError("Your cart is empty.")
|
||||||
|
@ -98,11 +100,11 @@ class InvoiceController(object):
|
||||||
product_items = product_items.order_by(
|
product_items = product_items.order_by(
|
||||||
"product__category__order", "product__order"
|
"product__category__order", "product__order"
|
||||||
)
|
)
|
||||||
discount_items = rego.DiscountItem.objects.filter(cart=cart)
|
discount_items = commerce.DiscountItem.objects.filter(cart=cart)
|
||||||
invoice_value = Decimal()
|
invoice_value = Decimal()
|
||||||
for item in product_items:
|
for item in product_items:
|
||||||
product = item.product
|
product = item.product
|
||||||
line_item = rego.LineItem.objects.create(
|
line_item = commerce.LineItem.objects.create(
|
||||||
invoice=invoice,
|
invoice=invoice,
|
||||||
description="%s - %s" % (product.category.name, product.name),
|
description="%s - %s" % (product.category.name, product.name),
|
||||||
quantity=item.quantity,
|
quantity=item.quantity,
|
||||||
|
@ -112,7 +114,7 @@ class InvoiceController(object):
|
||||||
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 = commerce.LineItem.objects.create(
|
||||||
invoice=invoice,
|
invoice=invoice,
|
||||||
description=item.discount.description,
|
description=item.discount.description,
|
||||||
quantity=item.quantity,
|
quantity=item.quantity,
|
||||||
|
@ -170,7 +172,7 @@ class InvoiceController(object):
|
||||||
def total_payments(self):
|
def total_payments(self):
|
||||||
''' Returns the total amount paid towards this invoice. '''
|
''' Returns the total amount paid towards this invoice. '''
|
||||||
|
|
||||||
payments = rego.PaymentBase.objects.filter(invoice=self.invoice)
|
payments = commerce.PaymentBase.objects.filter(invoice=self.invoice)
|
||||||
total_paid = payments.aggregate(Sum("amount"))["amount__sum"] or 0
|
total_paid = payments.aggregate(Sum("amount"))["amount__sum"] or 0
|
||||||
return total_paid
|
return total_paid
|
||||||
|
|
||||||
|
@ -180,12 +182,12 @@ class InvoiceController(object):
|
||||||
|
|
||||||
old_status = self.invoice.status
|
old_status = self.invoice.status
|
||||||
total_paid = self.total_payments()
|
total_paid = self.total_payments()
|
||||||
num_payments = rego.PaymentBase.objects.filter(
|
num_payments = commerce.PaymentBase.objects.filter(
|
||||||
invoice=self.invoice,
|
invoice=self.invoice,
|
||||||
).count()
|
).count()
|
||||||
remainder = self.invoice.value - total_paid
|
remainder = self.invoice.value - total_paid
|
||||||
|
|
||||||
if old_status == rego.Invoice.STATUS_UNPAID:
|
if old_status == commerce.Invoice.STATUS_UNPAID:
|
||||||
# Invoice had an amount owing
|
# Invoice had an amount owing
|
||||||
if remainder <= 0:
|
if remainder <= 0:
|
||||||
# Invoice no longer has amount owing
|
# Invoice no longer has amount owing
|
||||||
|
@ -199,15 +201,15 @@ class InvoiceController(object):
|
||||||
elif total_paid == 0 and num_payments > 0:
|
elif total_paid == 0 and num_payments > 0:
|
||||||
# Invoice has multiple payments totalling zero
|
# Invoice has multiple payments totalling zero
|
||||||
self._mark_void()
|
self._mark_void()
|
||||||
elif old_status == rego.Invoice.STATUS_PAID:
|
elif old_status == commerce.Invoice.STATUS_PAID:
|
||||||
if remainder > 0:
|
if remainder > 0:
|
||||||
# Invoice went from having a remainder of zero or less
|
# Invoice went from having a remainder of zero or less
|
||||||
# to having a positive remainder -- must be a refund
|
# to having a positive remainder -- must be a refund
|
||||||
self._mark_refunded()
|
self._mark_refunded()
|
||||||
elif old_status == rego.Invoice.STATUS_REFUNDED:
|
elif old_status == commerce.Invoice.STATUS_REFUNDED:
|
||||||
# Should not ever change from here
|
# Should not ever change from here
|
||||||
pass
|
pass
|
||||||
elif old_status == rego.Invoice.STATUS_VOID:
|
elif old_status == commerce.Invoice.STATUS_VOID:
|
||||||
# Should not ever change from here
|
# Should not ever change from here
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -218,7 +220,7 @@ class InvoiceController(object):
|
||||||
if cart:
|
if cart:
|
||||||
cart.active = False
|
cart.active = False
|
||||||
cart.save()
|
cart.save()
|
||||||
self.invoice.status = rego.Invoice.STATUS_PAID
|
self.invoice.status = commerce.Invoice.STATUS_PAID
|
||||||
self.invoice.save()
|
self.invoice.save()
|
||||||
|
|
||||||
def _mark_refunded(self):
|
def _mark_refunded(self):
|
||||||
|
@ -229,13 +231,13 @@ class InvoiceController(object):
|
||||||
cart.active = False
|
cart.active = False
|
||||||
cart.released = True
|
cart.released = True
|
||||||
cart.save()
|
cart.save()
|
||||||
self.invoice.status = rego.Invoice.STATUS_REFUNDED
|
self.invoice.status = commerce.Invoice.STATUS_REFUNDED
|
||||||
self.invoice.save()
|
self.invoice.save()
|
||||||
|
|
||||||
def _mark_void(self):
|
def _mark_void(self):
|
||||||
''' Marks the invoice as refunded, and updates the attached cart if
|
''' Marks the invoice as refunded, and updates the attached cart if
|
||||||
necessary. '''
|
necessary. '''
|
||||||
self.invoice.status = rego.Invoice.STATUS_VOID
|
self.invoice.status = commerce.Invoice.STATUS_VOID
|
||||||
self.invoice.save()
|
self.invoice.save()
|
||||||
|
|
||||||
def _invoice_matches_cart(self):
|
def _invoice_matches_cart(self):
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import inventory
|
||||||
|
|
||||||
from category import CategoryController
|
from category import CategoryController
|
||||||
from conditions import ConditionController
|
from conditions import ConditionController
|
||||||
|
@ -22,7 +23,7 @@ class ProductController(object):
|
||||||
raise ValueError("You must provide products or a category")
|
raise ValueError("You must provide products or a category")
|
||||||
|
|
||||||
if category is not None:
|
if category is not None:
|
||||||
all_products = rego.Product.objects.filter(category=category)
|
all_products = inventory.Product.objects.filter(category=category)
|
||||||
all_products = all_products.select_related("category")
|
all_products = all_products.select_related("category")
|
||||||
else:
|
else:
|
||||||
all_products = []
|
all_products = []
|
||||||
|
@ -65,13 +66,13 @@ class ProductController(object):
|
||||||
# Don't need to run the remaining queries
|
# Don't need to run the remaining queries
|
||||||
return 999999 # We can do better
|
return 999999 # We can do better
|
||||||
|
|
||||||
carts = rego.Cart.objects.filter(
|
carts = commerce.Cart.objects.filter(
|
||||||
user=user,
|
user=user,
|
||||||
active=False,
|
active=False,
|
||||||
released=False,
|
released=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
items = rego.ProductItem.objects.filter(
|
items = commerce.ProductItem.objects.filter(
|
||||||
cart__in=carts,
|
cart__in=carts,
|
||||||
product=self.product,
|
product=self.product,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import inventory
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
|
@ -14,8 +15,8 @@ class ApplyCreditNoteForm(forms.Form):
|
||||||
self.fields["invoice"].choices = self._unpaid_invoices_for_user
|
self.fields["invoice"].choices = self._unpaid_invoices_for_user
|
||||||
|
|
||||||
def _unpaid_invoices_for_user(self):
|
def _unpaid_invoices_for_user(self):
|
||||||
invoices = rego.Invoice.objects.filter(
|
invoices = commerce.Invoice.objects.filter(
|
||||||
status=rego.Invoice.STATUS_UNPAID,
|
status=commerce.Invoice.STATUS_UNPAID,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,7 +26,6 @@ class ApplyCreditNoteForm(forms.Form):
|
||||||
]
|
]
|
||||||
|
|
||||||
invoice = forms.ChoiceField(
|
invoice = forms.ChoiceField(
|
||||||
#choices=_unpaid_invoices_for_user,
|
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,14 +33,14 @@ class ApplyCreditNoteForm(forms.Form):
|
||||||
class ManualCreditNoteRefundForm(forms.ModelForm):
|
class ManualCreditNoteRefundForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = rego.ManualCreditNoteRefund
|
model = commerce.ManualCreditNoteRefund
|
||||||
fields = ["reference"]
|
fields = ["reference"]
|
||||||
|
|
||||||
|
|
||||||
class ManualPaymentForm(forms.ModelForm):
|
class ManualPaymentForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = rego.ManualPayment
|
model = commerce.ManualPayment
|
||||||
fields = ["reference", "amount"]
|
fields = ["reference", "amount"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -168,8 +168,8 @@ def ProductsForm(category, products):
|
||||||
|
|
||||||
# Each Category.RENDER_TYPE value has a subclass here.
|
# Each Category.RENDER_TYPE value has a subclass here.
|
||||||
RENDER_TYPES = {
|
RENDER_TYPES = {
|
||||||
rego.Category.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm,
|
inventory.Category.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm,
|
||||||
rego.Category.RENDER_TYPE_RADIO: _RadioButtonProductsForm,
|
inventory.Category.RENDER_TYPE_RADIO: _RadioButtonProductsForm,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Produce a subclass of _ProductsForm which we can alter the base_fields on
|
# Produce a subclass of _ProductsForm which we can alter the base_fields on
|
||||||
|
|
|
@ -1,818 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import util
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import itertools
|
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db import models
|
|
||||||
from django.db.models import F, Q
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from model_utils.managers import InheritanceManager
|
|
||||||
|
|
||||||
|
|
||||||
# User models
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Attendee(models.Model):
|
|
||||||
''' Miscellaneous user-related data. '''
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s" % self.user
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_instance(user):
|
|
||||||
''' Returns the instance of attendee for the given user, or creates
|
|
||||||
a new one. '''
|
|
||||||
try:
|
|
||||||
return Attendee.objects.get(user=user)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
return Attendee.objects.create(user=user)
|
|
||||||
|
|
||||||
def save(self, *a, **k):
|
|
||||||
while not self.access_code:
|
|
||||||
access_code = util.generate_access_code()
|
|
||||||
if Attendee.objects.filter(access_code=access_code).count() == 0:
|
|
||||||
self.access_code = access_code
|
|
||||||
return super(Attendee, self).save(*a, **k)
|
|
||||||
|
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
|
||||||
# Badge/profile is linked
|
|
||||||
access_code = models.CharField(
|
|
||||||
max_length=6,
|
|
||||||
unique=True,
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
completed_registration = models.BooleanField(default=False)
|
|
||||||
guided_categories_complete = models.ManyToManyField("category")
|
|
||||||
|
|
||||||
|
|
||||||
class AttendeeProfileBase(models.Model):
|
|
||||||
''' Information for an attendee's badge and related preferences.
|
|
||||||
Subclass this in your Django site to ask for attendee information in your
|
|
||||||
registration progess.
|
|
||||||
'''
|
|
||||||
|
|
||||||
objects = InheritanceManager()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def name_field(cls):
|
|
||||||
''' This is used to pre-fill the attendee's name from the
|
|
||||||
speaker profile. If it's None, that functionality is disabled. '''
|
|
||||||
return None
|
|
||||||
|
|
||||||
def invoice_recipient(self):
|
|
||||||
''' Returns a representation of this attendee profile for the purpose
|
|
||||||
of rendering to an invoice. Override in subclasses. '''
|
|
||||||
|
|
||||||
# Manual dispatch to subclass. Fleh.
|
|
||||||
slf = AttendeeProfileBase.objects.get_subclass(id=self.id)
|
|
||||||
# Actually compare the functions.
|
|
||||||
if type(slf).invoice_recipient != type(self).invoice_recipient:
|
|
||||||
return type(slf).invoice_recipient(slf)
|
|
||||||
|
|
||||||
# Return a default
|
|
||||||
return slf.attendee.user.username
|
|
||||||
|
|
||||||
attendee = models.OneToOneField(Attendee, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
|
|
||||||
# Inventory Models
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Category(models.Model):
|
|
||||||
''' Registration product categories '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("inventory - category")
|
|
||||||
verbose_name_plural = _("inventory - categories")
|
|
||||||
ordering = ("order", )
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
RENDER_TYPE_RADIO = 1
|
|
||||||
RENDER_TYPE_QUANTITY = 2
|
|
||||||
|
|
||||||
CATEGORY_RENDER_TYPES = [
|
|
||||||
(RENDER_TYPE_RADIO, _("Radio button")),
|
|
||||||
(RENDER_TYPE_QUANTITY, _("Quantity boxes")),
|
|
||||||
]
|
|
||||||
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=65,
|
|
||||||
verbose_name=_("Name"),
|
|
||||||
)
|
|
||||||
description = models.CharField(
|
|
||||||
max_length=255,
|
|
||||||
verbose_name=_("Description"),
|
|
||||||
)
|
|
||||||
limit_per_user = models.PositiveIntegerField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Limit per user"),
|
|
||||||
help_text=_("The total number of items from this category one "
|
|
||||||
"attendee may purchase."),
|
|
||||||
)
|
|
||||||
required = models.BooleanField(
|
|
||||||
blank=True,
|
|
||||||
help_text=_("If enabled, a user must select an "
|
|
||||||
"item from this category."),
|
|
||||||
)
|
|
||||||
order = models.PositiveIntegerField(
|
|
||||||
verbose_name=("Display order"),
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
render_type = models.IntegerField(
|
|
||||||
choices=CATEGORY_RENDER_TYPES,
|
|
||||||
verbose_name=_("Render type"),
|
|
||||||
help_text=_("The registration form will render this category in this "
|
|
||||||
"style."),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Product(models.Model):
|
|
||||||
''' Registration products '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("inventory - product")
|
|
||||||
ordering = ("category__order", "order")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s - %s" % (self.category.name, self.name)
|
|
||||||
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=65,
|
|
||||||
verbose_name=_("Name"),
|
|
||||||
)
|
|
||||||
description = models.CharField(
|
|
||||||
max_length=255,
|
|
||||||
verbose_name=_("Description"),
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
)
|
|
||||||
category = models.ForeignKey(
|
|
||||||
Category,
|
|
||||||
verbose_name=_("Product category")
|
|
||||||
)
|
|
||||||
price = models.DecimalField(
|
|
||||||
max_digits=8,
|
|
||||||
decimal_places=2,
|
|
||||||
verbose_name=_("Price"),
|
|
||||||
)
|
|
||||||
limit_per_user = models.PositiveIntegerField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Limit per user"),
|
|
||||||
)
|
|
||||||
reservation_duration = models.DurationField(
|
|
||||||
default=datetime.timedelta(hours=1),
|
|
||||||
verbose_name=_("Reservation duration"),
|
|
||||||
help_text=_("The length of time this product will be reserved before "
|
|
||||||
"it is released for someone else to purchase."),
|
|
||||||
)
|
|
||||||
order = models.PositiveIntegerField(
|
|
||||||
verbose_name=("Display order"),
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Voucher(models.Model):
|
|
||||||
''' Registration vouchers '''
|
|
||||||
|
|
||||||
# Vouchers reserve a cart for a fixed amount of time, so that
|
|
||||||
# items may be added without the voucher being swiped by someone else
|
|
||||||
RESERVATION_DURATION = datetime.timedelta(hours=1)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Voucher for %s" % self.recipient
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def normalise_code(cls, code):
|
|
||||||
return code.upper()
|
|
||||||
|
|
||||||
def save(self, *a, **k):
|
|
||||||
''' Normalise the voucher code to be uppercase '''
|
|
||||||
self.code = self.normalise_code(self.code)
|
|
||||||
super(Voucher, self).save(*a, **k)
|
|
||||||
|
|
||||||
recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
|
|
||||||
code = models.CharField(max_length=16,
|
|
||||||
unique=True,
|
|
||||||
verbose_name=_("Voucher code"))
|
|
||||||
limit = models.PositiveIntegerField(verbose_name=_("Voucher use limit"))
|
|
||||||
|
|
||||||
|
|
||||||
# Product Modifiers
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class DiscountBase(models.Model):
|
|
||||||
''' Base class for discounts. Each subclass has controller code that
|
|
||||||
determines whether or not the given discount is available to be added to
|
|
||||||
the current cart. '''
|
|
||||||
|
|
||||||
objects = InheritanceManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Discount: " + self.description
|
|
||||||
|
|
||||||
def effects(self):
|
|
||||||
''' Returns all of the effects of this discount. '''
|
|
||||||
products = self.discountforproduct_set.all()
|
|
||||||
categories = self.discountforcategory_set.all()
|
|
||||||
return itertools.chain(products, categories)
|
|
||||||
|
|
||||||
description = models.CharField(
|
|
||||||
max_length=255,
|
|
||||||
verbose_name=_("Description"),
|
|
||||||
help_text=_("A description of this discount. This will be included on "
|
|
||||||
"invoices where this discount is applied."),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class DiscountForProduct(models.Model):
|
|
||||||
''' Represents a discount on an individual product. Each Discount can
|
|
||||||
contain multiple products and categories. Discounts can either be a
|
|
||||||
percentage or a fixed amount, but not both. '''
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.percentage:
|
|
||||||
return "%s%% off %s" % (self.percentage, self.product)
|
|
||||||
elif self.price:
|
|
||||||
return "$%s off %s" % (self.price, self.product)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self.percentage is None and self.price is None:
|
|
||||||
raise ValidationError(
|
|
||||||
_("Discount must have a percentage or a price."))
|
|
||||||
elif self.percentage is not None and self.price is not None:
|
|
||||||
raise ValidationError(
|
|
||||||
_("Discount may only have a percentage or only a price."))
|
|
||||||
|
|
||||||
prods = DiscountForProduct.objects.filter(
|
|
||||||
discount=self.discount,
|
|
||||||
product=self.product)
|
|
||||||
cats = DiscountForCategory.objects.filter(
|
|
||||||
discount=self.discount,
|
|
||||||
category=self.product.category)
|
|
||||||
if len(prods) > 1:
|
|
||||||
raise ValidationError(
|
|
||||||
_("You may only have one discount line per product"))
|
|
||||||
if len(cats) != 0:
|
|
||||||
raise ValidationError(
|
|
||||||
_("You may only have one discount for "
|
|
||||||
"a product or its category"))
|
|
||||||
|
|
||||||
discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
|
|
||||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
|
||||||
percentage = models.DecimalField(
|
|
||||||
max_digits=4, decimal_places=1, null=True, blank=True)
|
|
||||||
price = models.DecimalField(
|
|
||||||
max_digits=8, decimal_places=2, null=True, blank=True)
|
|
||||||
quantity = models.PositiveIntegerField()
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class DiscountForCategory(models.Model):
|
|
||||||
''' Represents a discount for a category of products. Each discount can
|
|
||||||
contain multiple products. Category discounts can only be a percentage. '''
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s%% off %s" % (self.percentage, self.category)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
prods = DiscountForProduct.objects.filter(
|
|
||||||
discount=self.discount,
|
|
||||||
product__category=self.category)
|
|
||||||
cats = DiscountForCategory.objects.filter(
|
|
||||||
discount=self.discount,
|
|
||||||
category=self.category)
|
|
||||||
if len(prods) != 0:
|
|
||||||
raise ValidationError(
|
|
||||||
_("You may only have one discount for "
|
|
||||||
"a product or its category"))
|
|
||||||
if len(cats) > 1:
|
|
||||||
raise ValidationError(
|
|
||||||
_("You may only have one discount line per category"))
|
|
||||||
|
|
||||||
discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
|
|
||||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
|
||||||
percentage = models.DecimalField(
|
|
||||||
max_digits=4,
|
|
||||||
decimal_places=1)
|
|
||||||
quantity = models.PositiveIntegerField()
|
|
||||||
|
|
||||||
|
|
||||||
class TimeOrStockLimitDiscount(DiscountBase):
|
|
||||||
''' Discounts that are generally available, but are limited by timespan or
|
|
||||||
usage count. This is for e.g. Early Bird discounts. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("discount (time/stock limit)")
|
|
||||||
verbose_name_plural = _("discounts (time/stock limit)")
|
|
||||||
|
|
||||||
start_time = models.DateTimeField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Start time"),
|
|
||||||
help_text=_("This discount will only be available after this time."),
|
|
||||||
)
|
|
||||||
end_time = models.DateTimeField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("End time"),
|
|
||||||
help_text=_("This discount will only be available before this time."),
|
|
||||||
)
|
|
||||||
limit = models.PositiveIntegerField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Limit"),
|
|
||||||
help_text=_("This discount may only be applied this many times."),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VoucherDiscount(DiscountBase):
|
|
||||||
''' Discounts that are enabled when a voucher code is in the current
|
|
||||||
cart. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("discount (enabled by voucher)")
|
|
||||||
verbose_name_plural = _("discounts (enabled by voucher)")
|
|
||||||
|
|
||||||
voucher = models.OneToOneField(
|
|
||||||
Voucher,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_("Voucher"),
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IncludedProductDiscount(DiscountBase):
|
|
||||||
''' Discounts that are enabled because another product has been purchased.
|
|
||||||
e.g. A conference ticket includes a free t-shirt. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("discount (product inclusions)")
|
|
||||||
verbose_name_plural = _("discounts (product inclusions)")
|
|
||||||
|
|
||||||
enabling_products = models.ManyToManyField(
|
|
||||||
Product,
|
|
||||||
verbose_name=_("Including product"),
|
|
||||||
help_text=_("If one of these products are purchased, the discounts "
|
|
||||||
"below will be enabled."),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RoleDiscount(object):
|
|
||||||
''' Discounts that are enabled because the active user has a specific
|
|
||||||
role. This is for e.g. volunteers who can get a discount ticket. '''
|
|
||||||
# TODO: implement RoleDiscount
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class FlagBase(models.Model):
|
|
||||||
''' This defines a condition which allows products or categories to
|
|
||||||
be made visible, or be prevented from being visible.
|
|
||||||
|
|
||||||
The various subclasses of this can define the conditions that enable
|
|
||||||
or disable products, by the following rules:
|
|
||||||
|
|
||||||
If there is at least one 'disable if false' flag defined on a product or
|
|
||||||
category, all such flag conditions must be met. If there is at least one
|
|
||||||
'enable if true' flag, at least one such condition must be met.
|
|
||||||
|
|
||||||
If both types of conditions exist on a product, both of these rules apply.
|
|
||||||
'''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
# TODO: make concrete once https://code.djangoproject.com/ticket/26488
|
|
||||||
# is solved.
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
DISABLE_IF_FALSE = 1
|
|
||||||
ENABLE_IF_TRUE = 2
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.description
|
|
||||||
|
|
||||||
def effects(self):
|
|
||||||
''' Returns all of the items affected by this condition. '''
|
|
||||||
return itertools.chain(self.products.all(), self.categories.all())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_disable_if_false(self):
|
|
||||||
return self.condition == FlagBase.DISABLE_IF_FALSE
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_enable_if_true(self):
|
|
||||||
return self.condition == FlagBase.ENABLE_IF_TRUE
|
|
||||||
|
|
||||||
description = models.CharField(max_length=255)
|
|
||||||
condition = models.IntegerField(
|
|
||||||
default=ENABLE_IF_TRUE,
|
|
||||||
choices=(
|
|
||||||
(DISABLE_IF_FALSE, _("Disable if false")),
|
|
||||||
(ENABLE_IF_TRUE, _("Enable if true")),
|
|
||||||
),
|
|
||||||
help_text=_("If there is at least one 'disable if false' flag "
|
|
||||||
"defined on a product or category, all such flag "
|
|
||||||
" conditions must be met. If there is at least one "
|
|
||||||
"'enable if true' flag, at least one such condition must "
|
|
||||||
"be met. If both types of conditions exist on a product, "
|
|
||||||
"both of these rules apply."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
products = models.ManyToManyField(
|
|
||||||
Product,
|
|
||||||
blank=True,
|
|
||||||
help_text=_("Products affected by this flag's condition."),
|
|
||||||
related_name="flagbase_set",
|
|
||||||
)
|
|
||||||
categories = models.ManyToManyField(
|
|
||||||
Category,
|
|
||||||
blank=True,
|
|
||||||
help_text=_("Categories whose products are affected by this flag's "
|
|
||||||
"condition."
|
|
||||||
),
|
|
||||||
related_name="flagbase_set",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EnablingConditionBase(FlagBase):
|
|
||||||
''' Reifies the abstract FlagBase. This is necessary because django
|
|
||||||
prevents renaming base classes in migrations. '''
|
|
||||||
# TODO: remove this, and make subclasses subclass FlagBase once
|
|
||||||
# https://code.djangoproject.com/ticket/26488 is solved.
|
|
||||||
|
|
||||||
objects = InheritanceManager()
|
|
||||||
|
|
||||||
|
|
||||||
class TimeOrStockLimitFlag(EnablingConditionBase):
|
|
||||||
''' Registration product ceilings '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("flag (time/stock limit)")
|
|
||||||
verbose_name_plural = _("flags (time/stock limit)")
|
|
||||||
|
|
||||||
start_time = models.DateTimeField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
help_text=_("Products included in this condition will only be "
|
|
||||||
"available after this time."),
|
|
||||||
)
|
|
||||||
end_time = models.DateTimeField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
help_text=_("Products included in this condition will only be "
|
|
||||||
"available before this time."),
|
|
||||||
)
|
|
||||||
limit = models.PositiveIntegerField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
help_text=_("The number of items under this grouping that can be "
|
|
||||||
"purchased."),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class ProductFlag(EnablingConditionBase):
|
|
||||||
''' The condition is met because a specific product is purchased. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("flag (dependency on product)")
|
|
||||||
verbose_name_plural = _("flags (dependency on product)")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Enabled by products: " + str(self.enabling_products.all())
|
|
||||||
|
|
||||||
enabling_products = models.ManyToManyField(
|
|
||||||
Product,
|
|
||||||
help_text=_("If one of these products are purchased, this condition "
|
|
||||||
"is met."),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class CategoryFlag(EnablingConditionBase):
|
|
||||||
''' The condition is met because a product in a particular product is
|
|
||||||
purchased. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("flag (dependency on product from category)")
|
|
||||||
verbose_name_plural = _("flags (dependency on product from category)")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Enabled by product in category: " + str(self.enabling_category)
|
|
||||||
|
|
||||||
enabling_category = models.ForeignKey(
|
|
||||||
Category,
|
|
||||||
help_text=_("If a product from this category is purchased, this "
|
|
||||||
"condition is met."),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class VoucherFlag(EnablingConditionBase):
|
|
||||||
''' The condition is met because a Voucher is present. This is for e.g.
|
|
||||||
enabling sponsor tickets. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("flag (dependency on voucher)")
|
|
||||||
verbose_name_plural = _("flags (dependency on voucher)")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Enabled by voucher: %s" % self.voucher
|
|
||||||
|
|
||||||
voucher = models.OneToOneField(Voucher)
|
|
||||||
|
|
||||||
|
|
||||||
# @python_2_unicode_compatible
|
|
||||||
class RoleFlag(object):
|
|
||||||
''' The condition is met because the active user has a particular Role.
|
|
||||||
This is for e.g. enabling Team tickets. '''
|
|
||||||
# TODO: implement RoleFlag
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Commerce Models
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Cart(models.Model):
|
|
||||||
''' Represents a set of product items that have been purchased, or are
|
|
||||||
pending purchase. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
index_together = [
|
|
||||||
("active", "time_last_updated"),
|
|
||||||
("active", "released"),
|
|
||||||
("active", "user"),
|
|
||||||
("released", "user"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%d rev #%d" % (self.id, self.revision)
|
|
||||||
|
|
||||||
user = models.ForeignKey(User)
|
|
||||||
# ProductItems (foreign key)
|
|
||||||
vouchers = models.ManyToManyField(Voucher, blank=True)
|
|
||||||
time_last_updated = models.DateTimeField(
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
reservation_duration = models.DurationField()
|
|
||||||
revision = models.PositiveIntegerField(default=1)
|
|
||||||
active = models.BooleanField(
|
|
||||||
default=True,
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
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(time_last_updated__gt=(
|
|
||||||
timezone.now()-F('reservation_duration')
|
|
||||||
))) |
|
|
||||||
(Q(active=False) & Q(released=False))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class ProductItem(models.Model):
|
|
||||||
''' Represents a product-quantity pair in a Cart. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("product", )
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "product: %s * %d in Cart: %s" % (
|
|
||||||
self.product, self.quantity, self.cart)
|
|
||||||
|
|
||||||
cart = models.ForeignKey(Cart)
|
|
||||||
product = models.ForeignKey(Product)
|
|
||||||
quantity = models.PositiveIntegerField(db_index=True)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class DiscountItem(models.Model):
|
|
||||||
''' Represents a discount-product-quantity relation in a Cart. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("product", )
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s: %s * %d in Cart: %s" % (
|
|
||||||
self.discount, self.product, self.quantity, self.cart)
|
|
||||||
|
|
||||||
cart = models.ForeignKey(Cart)
|
|
||||||
product = models.ForeignKey(Product)
|
|
||||||
discount = models.ForeignKey(DiscountBase)
|
|
||||||
quantity = models.PositiveIntegerField()
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Invoice(models.Model):
|
|
||||||
''' An invoice. Invoices can be automatically generated when checking out
|
|
||||||
a Cart, in which case, it is attached to a given revision of a Cart. '''
|
|
||||||
|
|
||||||
STATUS_UNPAID = 1
|
|
||||||
STATUS_PAID = 2
|
|
||||||
STATUS_REFUNDED = 3
|
|
||||||
STATUS_VOID = 4
|
|
||||||
|
|
||||||
STATUS_TYPES = [
|
|
||||||
(STATUS_UNPAID, _("Unpaid")),
|
|
||||||
(STATUS_PAID, _("Paid")),
|
|
||||||
(STATUS_REFUNDED, _("Refunded")),
|
|
||||||
(STATUS_VOID, _("VOID")),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Invoice #%d" % self.id
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self.cart is not None and self.cart_revision is None:
|
|
||||||
raise ValidationError(
|
|
||||||
"If this is a cart invoice, it must have a revision")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_unpaid(self):
|
|
||||||
return self.status == self.STATUS_UNPAID
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_void(self):
|
|
||||||
return self.status == self.STATUS_VOID
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_paid(self):
|
|
||||||
return self.status == self.STATUS_PAID
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_refunded(self):
|
|
||||||
return self.status == self.STATUS_REFUNDED
|
|
||||||
|
|
||||||
# Invoice Number
|
|
||||||
user = models.ForeignKey(User)
|
|
||||||
cart = models.ForeignKey(Cart, null=True)
|
|
||||||
cart_revision = models.IntegerField(
|
|
||||||
null=True,
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
# Line Items (foreign key)
|
|
||||||
status = models.IntegerField(
|
|
||||||
choices=STATUS_TYPES,
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
recipient = models.CharField(max_length=1024)
|
|
||||||
issue_time = models.DateTimeField()
|
|
||||||
due_time = models.DateTimeField()
|
|
||||||
value = models.DecimalField(max_digits=8, decimal_places=2)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class LineItem(models.Model):
|
|
||||||
''' Line items for an invoice. These are denormalised from the ProductItems
|
|
||||||
and DiscountItems that belong to a cart (for consistency), but also allow
|
|
||||||
for arbitrary line items when required. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("id", )
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Line: %s * %d @ %s" % (
|
|
||||||
self.description, self.quantity, self.price)
|
|
||||||
|
|
||||||
invoice = models.ForeignKey(Invoice)
|
|
||||||
description = models.CharField(max_length=255)
|
|
||||||
quantity = models.PositiveIntegerField()
|
|
||||||
price = models.DecimalField(max_digits=8, decimal_places=2)
|
|
||||||
product = models.ForeignKey(Product, null=True, blank=True)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class PaymentBase(models.Model):
|
|
||||||
''' The base payment type for invoices. Payment apps should subclass this
|
|
||||||
class to handle implementation-specific issues. '''
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("time", )
|
|
||||||
|
|
||||||
objects = InheritanceManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Payment: ref=%s amount=%s" % (self.reference, self.amount)
|
|
||||||
|
|
||||||
invoice = models.ForeignKey(Invoice)
|
|
||||||
time = models.DateTimeField(default=timezone.now)
|
|
||||||
reference = models.CharField(max_length=255)
|
|
||||||
amount = models.DecimalField(max_digits=8, decimal_places=2)
|
|
||||||
|
|
||||||
|
|
||||||
class ManualPayment(PaymentBase):
|
|
||||||
''' Payments that are manually entered by staff. '''
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CreditNote(PaymentBase):
|
|
||||||
''' Credit notes represent money accounted for in the system that do not
|
|
||||||
belong to specific invoices. They may be paid into other invoices, or
|
|
||||||
cashed out as refunds.
|
|
||||||
|
|
||||||
Each CreditNote may either be used to pay towards another Invoice in the
|
|
||||||
system (by attaching a CreditNoteApplication), or may be marked as
|
|
||||||
refunded (by attaching a CreditNoteRefund).'''
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def unclaimed(cls):
|
|
||||||
return cls.objects.filter(
|
|
||||||
creditnoteapplication=None,
|
|
||||||
creditnoterefund=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self):
|
|
||||||
if self.is_unclaimed:
|
|
||||||
return "Unclaimed"
|
|
||||||
|
|
||||||
if hasattr(self, 'creditnoteapplication'):
|
|
||||||
destination = self.creditnoteapplication.invoice.id
|
|
||||||
return "Applied to invoice %d" % destination
|
|
||||||
|
|
||||||
elif hasattr(self, 'creditnoterefund'):
|
|
||||||
reference = self.creditnoterefund.reference
|
|
||||||
print reference
|
|
||||||
return "Refunded with reference: %s" % reference
|
|
||||||
|
|
||||||
raise ValueError("This should never happen.")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_unclaimed(self):
|
|
||||||
return not (
|
|
||||||
hasattr(self, 'creditnoterefund') or
|
|
||||||
hasattr(self, 'creditnoteapplication')
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
''' Returns the value of the credit note. Because CreditNotes are
|
|
||||||
implemented as PaymentBase objects internally, the amount is a
|
|
||||||
negative payment against an invoice. '''
|
|
||||||
return -self.amount
|
|
||||||
|
|
||||||
|
|
||||||
class CleanOnSave(object):
|
|
||||||
|
|
||||||
def save(self, *a, **k):
|
|
||||||
self.full_clean()
|
|
||||||
super(CleanOnSave, self).save(*a, **k)
|
|
||||||
|
|
||||||
|
|
||||||
class CreditNoteApplication(CleanOnSave, PaymentBase):
|
|
||||||
''' Represents an application of a credit note to an Invoice. '''
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if not hasattr(self, "parent"):
|
|
||||||
return
|
|
||||||
if hasattr(self.parent, 'creditnoterefund'):
|
|
||||||
raise ValidationError(
|
|
||||||
"Cannot apply a refunded credit note to an invoice"
|
|
||||||
)
|
|
||||||
|
|
||||||
parent = models.OneToOneField(CreditNote)
|
|
||||||
|
|
||||||
|
|
||||||
class CreditNoteRefund(CleanOnSave, models.Model):
|
|
||||||
''' Represents a refund of a credit note to an external payment.
|
|
||||||
Credit notes may only be refunded in full. How those refunds are handled
|
|
||||||
is left as an exercise to the payment app. '''
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if not hasattr(self, "parent"):
|
|
||||||
return
|
|
||||||
if hasattr(self.parent, 'creditnoteapplication'):
|
|
||||||
raise ValidationError(
|
|
||||||
"Cannot refund a credit note that has been paid to an invoice"
|
|
||||||
)
|
|
||||||
|
|
||||||
parent = models.OneToOneField(CreditNote)
|
|
||||||
time = models.DateTimeField(default=timezone.now)
|
|
||||||
reference = models.CharField(max_length=255)
|
|
||||||
|
|
||||||
|
|
||||||
class ManualCreditNoteRefund(CreditNoteRefund):
|
|
||||||
''' Credit notes that are entered by a staff member. '''
|
|
||||||
|
|
||||||
entered_by = models.ForeignKey(User)
|
|
4
registrasion/models/__init__.py
Normal file
4
registrasion/models/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from commerce import * # NOQA
|
||||||
|
from conditions import * # NOQA
|
||||||
|
from inventory import * # NOQA
|
||||||
|
from people import * # NOQA
|
304
registrasion/models/commerce.py
Normal file
304
registrasion/models/commerce.py
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
from . import conditions
|
||||||
|
from . import inventory
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import F, Q
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from model_utils.managers import InheritanceManager
|
||||||
|
|
||||||
|
|
||||||
|
# Commerce Models
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Cart(models.Model):
|
||||||
|
''' Represents a set of product items that have been purchased, or are
|
||||||
|
pending purchase. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
index_together = [
|
||||||
|
("active", "time_last_updated"),
|
||||||
|
("active", "released"),
|
||||||
|
("active", "user"),
|
||||||
|
("released", "user"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%d rev #%d" % (self.id, self.revision)
|
||||||
|
|
||||||
|
user = models.ForeignKey(User)
|
||||||
|
# ProductItems (foreign key)
|
||||||
|
vouchers = models.ManyToManyField(inventory.Voucher, blank=True)
|
||||||
|
time_last_updated = models.DateTimeField(
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
reservation_duration = models.DurationField()
|
||||||
|
revision = models.PositiveIntegerField(default=1)
|
||||||
|
active = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
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(time_last_updated__gt=(
|
||||||
|
timezone.now()-F('reservation_duration')
|
||||||
|
))) |
|
||||||
|
(Q(active=False) & Q(released=False))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class ProductItem(models.Model):
|
||||||
|
''' Represents a product-quantity pair in a Cart. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
ordering = ("product", )
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "product: %s * %d in Cart: %s" % (
|
||||||
|
self.product, self.quantity, self.cart)
|
||||||
|
|
||||||
|
cart = models.ForeignKey(Cart)
|
||||||
|
product = models.ForeignKey(inventory.Product)
|
||||||
|
quantity = models.PositiveIntegerField(db_index=True)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class DiscountItem(models.Model):
|
||||||
|
''' Represents a discount-product-quantity relation in a Cart. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
ordering = ("product", )
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s: %s * %d in Cart: %s" % (
|
||||||
|
self.discount, self.product, self.quantity, self.cart)
|
||||||
|
|
||||||
|
cart = models.ForeignKey(Cart)
|
||||||
|
product = models.ForeignKey(inventory.Product)
|
||||||
|
discount = models.ForeignKey(conditions.DiscountBase)
|
||||||
|
quantity = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Invoice(models.Model):
|
||||||
|
''' An invoice. Invoices can be automatically generated when checking out
|
||||||
|
a Cart, in which case, it is attached to a given revision of a Cart. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
STATUS_UNPAID = 1
|
||||||
|
STATUS_PAID = 2
|
||||||
|
STATUS_REFUNDED = 3
|
||||||
|
STATUS_VOID = 4
|
||||||
|
|
||||||
|
STATUS_TYPES = [
|
||||||
|
(STATUS_UNPAID, _("Unpaid")),
|
||||||
|
(STATUS_PAID, _("Paid")),
|
||||||
|
(STATUS_REFUNDED, _("Refunded")),
|
||||||
|
(STATUS_VOID, _("VOID")),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Invoice #%d" % self.id
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.cart is not None and self.cart_revision is None:
|
||||||
|
raise ValidationError(
|
||||||
|
"If this is a cart invoice, it must have a revision")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_unpaid(self):
|
||||||
|
return self.status == self.STATUS_UNPAID
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_void(self):
|
||||||
|
return self.status == self.STATUS_VOID
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_paid(self):
|
||||||
|
return self.status == self.STATUS_PAID
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_refunded(self):
|
||||||
|
return self.status == self.STATUS_REFUNDED
|
||||||
|
|
||||||
|
# Invoice Number
|
||||||
|
user = models.ForeignKey(User)
|
||||||
|
cart = models.ForeignKey(Cart, null=True)
|
||||||
|
cart_revision = models.IntegerField(
|
||||||
|
null=True,
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
# Line Items (foreign key)
|
||||||
|
status = models.IntegerField(
|
||||||
|
choices=STATUS_TYPES,
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
recipient = models.CharField(max_length=1024)
|
||||||
|
issue_time = models.DateTimeField()
|
||||||
|
due_time = models.DateTimeField()
|
||||||
|
value = models.DecimalField(max_digits=8, decimal_places=2)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class LineItem(models.Model):
|
||||||
|
''' Line items for an invoice. These are denormalised from the ProductItems
|
||||||
|
and DiscountItems that belong to a cart (for consistency), but also allow
|
||||||
|
for arbitrary line items when required. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
ordering = ("id", )
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Line: %s * %d @ %s" % (
|
||||||
|
self.description, self.quantity, self.price)
|
||||||
|
|
||||||
|
invoice = models.ForeignKey(Invoice)
|
||||||
|
description = models.CharField(max_length=255)
|
||||||
|
quantity = models.PositiveIntegerField()
|
||||||
|
price = models.DecimalField(max_digits=8, decimal_places=2)
|
||||||
|
product = models.ForeignKey(inventory.Product, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class PaymentBase(models.Model):
|
||||||
|
''' The base payment type for invoices. Payment apps should subclass this
|
||||||
|
class to handle implementation-specific issues. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("time", )
|
||||||
|
|
||||||
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Payment: ref=%s amount=%s" % (self.reference, self.amount)
|
||||||
|
|
||||||
|
invoice = models.ForeignKey(Invoice)
|
||||||
|
time = models.DateTimeField(default=timezone.now)
|
||||||
|
reference = models.CharField(max_length=255)
|
||||||
|
amount = models.DecimalField(max_digits=8, decimal_places=2)
|
||||||
|
|
||||||
|
|
||||||
|
class ManualPayment(PaymentBase):
|
||||||
|
''' Payments that are manually entered by staff. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
|
||||||
|
class CreditNote(PaymentBase):
|
||||||
|
''' Credit notes represent money accounted for in the system that do not
|
||||||
|
belong to specific invoices. They may be paid into other invoices, or
|
||||||
|
cashed out as refunds.
|
||||||
|
|
||||||
|
Each CreditNote may either be used to pay towards another Invoice in the
|
||||||
|
system (by attaching a CreditNoteApplication), or may be marked as
|
||||||
|
refunded (by attaching a CreditNoteRefund).'''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def unclaimed(cls):
|
||||||
|
return cls.objects.filter(
|
||||||
|
creditnoteapplication=None,
|
||||||
|
creditnoterefund=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
if self.is_unclaimed:
|
||||||
|
return "Unclaimed"
|
||||||
|
|
||||||
|
if hasattr(self, 'creditnoteapplication'):
|
||||||
|
destination = self.creditnoteapplication.invoice.id
|
||||||
|
return "Applied to invoice %d" % destination
|
||||||
|
|
||||||
|
elif hasattr(self, 'creditnoterefund'):
|
||||||
|
reference = self.creditnoterefund.reference
|
||||||
|
print reference
|
||||||
|
return "Refunded with reference: %s" % reference
|
||||||
|
|
||||||
|
raise ValueError("This should never happen.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_unclaimed(self):
|
||||||
|
return not (
|
||||||
|
hasattr(self, 'creditnoterefund') or
|
||||||
|
hasattr(self, 'creditnoteapplication')
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
''' Returns the value of the credit note. Because CreditNotes are
|
||||||
|
implemented as PaymentBase objects internally, the amount is a
|
||||||
|
negative payment against an invoice. '''
|
||||||
|
return -self.amount
|
||||||
|
|
||||||
|
|
||||||
|
class CleanOnSave(object):
|
||||||
|
|
||||||
|
def save(self, *a, **k):
|
||||||
|
self.full_clean()
|
||||||
|
super(CleanOnSave, self).save(*a, **k)
|
||||||
|
|
||||||
|
|
||||||
|
class CreditNoteApplication(CleanOnSave, PaymentBase):
|
||||||
|
''' Represents an application of a credit note to an Invoice. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not hasattr(self, "parent"):
|
||||||
|
return
|
||||||
|
if hasattr(self.parent, 'creditnoterefund'):
|
||||||
|
raise ValidationError(
|
||||||
|
"Cannot apply a refunded credit note to an invoice"
|
||||||
|
)
|
||||||
|
|
||||||
|
parent = models.OneToOneField(CreditNote)
|
||||||
|
|
||||||
|
|
||||||
|
class CreditNoteRefund(CleanOnSave, models.Model):
|
||||||
|
''' Represents a refund of a credit note to an external payment.
|
||||||
|
Credit notes may only be refunded in full. How those refunds are handled
|
||||||
|
is left as an exercise to the payment app. '''
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not hasattr(self, "parent"):
|
||||||
|
return
|
||||||
|
if hasattr(self.parent, 'creditnoteapplication'):
|
||||||
|
raise ValidationError(
|
||||||
|
"Cannot refund a credit note that has been paid to an invoice"
|
||||||
|
)
|
||||||
|
|
||||||
|
parent = models.OneToOneField(CreditNote)
|
||||||
|
time = models.DateTimeField(default=timezone.now)
|
||||||
|
reference = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
|
||||||
|
class ManualCreditNoteRefund(CreditNoteRefund):
|
||||||
|
''' Credit notes that are entered by a staff member. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
entered_by = models.ForeignKey(User)
|
361
registrasion/models/conditions.py
Normal file
361
registrasion/models/conditions.py
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from . import inventory
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from model_utils.managers import InheritanceManager
|
||||||
|
|
||||||
|
|
||||||
|
# Product Modifiers
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class DiscountBase(models.Model):
|
||||||
|
''' Base class for discounts. Each subclass has controller code that
|
||||||
|
determines whether or not the given discount is available to be added to
|
||||||
|
the current cart. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Discount: " + self.description
|
||||||
|
|
||||||
|
def effects(self):
|
||||||
|
''' Returns all of the effects of this discount. '''
|
||||||
|
products = self.discountforproduct_set.all()
|
||||||
|
categories = self.discountforcategory_set.all()
|
||||||
|
return itertools.chain(products, categories)
|
||||||
|
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name=_("Description"),
|
||||||
|
help_text=_("A description of this discount. This will be included on "
|
||||||
|
"invoices where this discount is applied."),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class DiscountForProduct(models.Model):
|
||||||
|
''' Represents a discount on an individual product. Each Discount can
|
||||||
|
contain multiple products and categories. Discounts can either be a
|
||||||
|
percentage or a fixed amount, but not both. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.percentage:
|
||||||
|
return "%s%% off %s" % (self.percentage, self.product)
|
||||||
|
elif self.price:
|
||||||
|
return "$%s off %s" % (self.price, self.product)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.percentage is None and self.price is None:
|
||||||
|
raise ValidationError(
|
||||||
|
_("Discount must have a percentage or a price."))
|
||||||
|
elif self.percentage is not None and self.price is not None:
|
||||||
|
raise ValidationError(
|
||||||
|
_("Discount may only have a percentage or only a price."))
|
||||||
|
|
||||||
|
prods = DiscountForProduct.objects.filter(
|
||||||
|
discount=self.discount,
|
||||||
|
product=self.product)
|
||||||
|
cats = DiscountForCategory.objects.filter(
|
||||||
|
discount=self.discount,
|
||||||
|
category=self.product.category)
|
||||||
|
if len(prods) > 1:
|
||||||
|
raise ValidationError(
|
||||||
|
_("You may only have one discount line per product"))
|
||||||
|
if len(cats) != 0:
|
||||||
|
raise ValidationError(
|
||||||
|
_("You may only have one discount for "
|
||||||
|
"a product or its category"))
|
||||||
|
|
||||||
|
discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
|
||||||
|
product = models.ForeignKey(inventory.Product, on_delete=models.CASCADE)
|
||||||
|
percentage = models.DecimalField(
|
||||||
|
max_digits=4, decimal_places=1, null=True, blank=True)
|
||||||
|
price = models.DecimalField(
|
||||||
|
max_digits=8, decimal_places=2, null=True, blank=True)
|
||||||
|
quantity = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class DiscountForCategory(models.Model):
|
||||||
|
''' Represents a discount for a category of products. Each discount can
|
||||||
|
contain multiple products. Category discounts can only be a percentage. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s%% off %s" % (self.percentage, self.category)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
prods = DiscountForProduct.objects.filter(
|
||||||
|
discount=self.discount,
|
||||||
|
product__category=self.category)
|
||||||
|
cats = DiscountForCategory.objects.filter(
|
||||||
|
discount=self.discount,
|
||||||
|
category=self.category)
|
||||||
|
if len(prods) != 0:
|
||||||
|
raise ValidationError(
|
||||||
|
_("You may only have one discount for "
|
||||||
|
"a product or its category"))
|
||||||
|
if len(cats) > 1:
|
||||||
|
raise ValidationError(
|
||||||
|
_("You may only have one discount line per category"))
|
||||||
|
|
||||||
|
discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
|
||||||
|
category = models.ForeignKey(inventory.Category, on_delete=models.CASCADE)
|
||||||
|
percentage = models.DecimalField(
|
||||||
|
max_digits=4,
|
||||||
|
decimal_places=1)
|
||||||
|
quantity = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
|
||||||
|
class TimeOrStockLimitDiscount(DiscountBase):
|
||||||
|
''' Discounts that are generally available, but are limited by timespan or
|
||||||
|
usage count. This is for e.g. Early Bird discounts. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
verbose_name = _("discount (time/stock limit)")
|
||||||
|
verbose_name_plural = _("discounts (time/stock limit)")
|
||||||
|
|
||||||
|
start_time = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Start time"),
|
||||||
|
help_text=_("This discount will only be available after this time."),
|
||||||
|
)
|
||||||
|
end_time = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("End time"),
|
||||||
|
help_text=_("This discount will only be available before this time."),
|
||||||
|
)
|
||||||
|
limit = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Limit"),
|
||||||
|
help_text=_("This discount may only be applied this many times."),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VoucherDiscount(DiscountBase):
|
||||||
|
''' Discounts that are enabled when a voucher code is in the current
|
||||||
|
cart. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
verbose_name = _("discount (enabled by voucher)")
|
||||||
|
verbose_name_plural = _("discounts (enabled by voucher)")
|
||||||
|
|
||||||
|
voucher = models.OneToOneField(
|
||||||
|
inventory.Voucher,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("Voucher"),
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IncludedProductDiscount(DiscountBase):
|
||||||
|
''' Discounts that are enabled because another product has been purchased.
|
||||||
|
e.g. A conference ticket includes a free t-shirt. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
verbose_name = _("discount (product inclusions)")
|
||||||
|
verbose_name_plural = _("discounts (product inclusions)")
|
||||||
|
|
||||||
|
enabling_products = models.ManyToManyField(
|
||||||
|
inventory.Product,
|
||||||
|
verbose_name=_("Including product"),
|
||||||
|
help_text=_("If one of these products are purchased, the discounts "
|
||||||
|
"below will be enabled."),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDiscount(object):
|
||||||
|
''' Discounts that are enabled because the active user has a specific
|
||||||
|
role. This is for e.g. volunteers who can get a discount ticket. '''
|
||||||
|
# TODO: implement RoleDiscount
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class FlagBase(models.Model):
|
||||||
|
''' This defines a condition which allows products or categories to
|
||||||
|
be made visible, or be prevented from being visible.
|
||||||
|
|
||||||
|
The various subclasses of this can define the conditions that enable
|
||||||
|
or disable products, by the following rules:
|
||||||
|
|
||||||
|
If there is at least one 'disable if false' flag defined on a product or
|
||||||
|
category, all such flag conditions must be met. If there is at least one
|
||||||
|
'enable if true' flag, at least one such condition must be met.
|
||||||
|
|
||||||
|
If both types of conditions exist on a product, both of these rules apply.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
# TODO: make concrete once https://code.djangoproject.com/ticket/26488
|
||||||
|
# is solved.
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
DISABLE_IF_FALSE = 1
|
||||||
|
ENABLE_IF_TRUE = 2
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.description
|
||||||
|
|
||||||
|
def effects(self):
|
||||||
|
''' Returns all of the items affected by this condition. '''
|
||||||
|
return itertools.chain(self.products.all(), self.categories.all())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_disable_if_false(self):
|
||||||
|
return self.condition == FlagBase.DISABLE_IF_FALSE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_enable_if_true(self):
|
||||||
|
return self.condition == FlagBase.ENABLE_IF_TRUE
|
||||||
|
|
||||||
|
description = models.CharField(max_length=255)
|
||||||
|
condition = models.IntegerField(
|
||||||
|
default=ENABLE_IF_TRUE,
|
||||||
|
choices=(
|
||||||
|
(DISABLE_IF_FALSE, _("Disable if false")),
|
||||||
|
(ENABLE_IF_TRUE, _("Enable if true")),
|
||||||
|
),
|
||||||
|
help_text=_("If there is at least one 'disable if false' flag "
|
||||||
|
"defined on a product or category, all such flag "
|
||||||
|
" conditions must be met. If there is at least one "
|
||||||
|
"'enable if true' flag, at least one such condition must "
|
||||||
|
"be met. If both types of conditions exist on a product, "
|
||||||
|
"both of these rules apply."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
products = models.ManyToManyField(
|
||||||
|
inventory.Product,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Products affected by this flag's condition."),
|
||||||
|
related_name="flagbase_set",
|
||||||
|
)
|
||||||
|
categories = models.ManyToManyField(
|
||||||
|
inventory.Category,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Categories whose products are affected by this flag's "
|
||||||
|
"condition."
|
||||||
|
),
|
||||||
|
related_name="flagbase_set",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EnablingConditionBase(FlagBase):
|
||||||
|
''' Reifies the abstract FlagBase. This is necessary because django
|
||||||
|
prevents renaming base classes in migrations. '''
|
||||||
|
# TODO: remove this, and make subclasses subclass FlagBase once
|
||||||
|
# https://code.djangoproject.com/ticket/26488 is solved.
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
|
||||||
|
class TimeOrStockLimitFlag(EnablingConditionBase):
|
||||||
|
''' Registration product ceilings '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
verbose_name = _("flag (time/stock limit)")
|
||||||
|
verbose_name_plural = _("flags (time/stock limit)")
|
||||||
|
|
||||||
|
start_time = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Products included in this condition will only be "
|
||||||
|
"available after this time."),
|
||||||
|
)
|
||||||
|
end_time = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Products included in this condition will only be "
|
||||||
|
"available before this time."),
|
||||||
|
)
|
||||||
|
limit = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("The number of items under this grouping that can be "
|
||||||
|
"purchased."),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class ProductFlag(EnablingConditionBase):
|
||||||
|
''' The condition is met because a specific product is purchased. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
verbose_name = _("flag (dependency on product)")
|
||||||
|
verbose_name_plural = _("flags (dependency on product)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Enabled by products: " + str(self.enabling_products.all())
|
||||||
|
|
||||||
|
enabling_products = models.ManyToManyField(
|
||||||
|
inventory.Product,
|
||||||
|
help_text=_("If one of these products are purchased, this condition "
|
||||||
|
"is met."),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class CategoryFlag(EnablingConditionBase):
|
||||||
|
''' The condition is met because a product in a particular product is
|
||||||
|
purchased. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
verbose_name = _("flag (dependency on product from category)")
|
||||||
|
verbose_name_plural = _("flags (dependency on product from category)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Enabled by product in category: " + str(self.enabling_category)
|
||||||
|
|
||||||
|
enabling_category = models.ForeignKey(
|
||||||
|
inventory.Category,
|
||||||
|
help_text=_("If a product from this category is purchased, this "
|
||||||
|
"condition is met."),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class VoucherFlag(EnablingConditionBase):
|
||||||
|
''' The condition is met because a Voucher is present. This is for e.g.
|
||||||
|
enabling sponsor tickets. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
verbose_name = _("flag (dependency on voucher)")
|
||||||
|
verbose_name_plural = _("flags (dependency on voucher)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Enabled by voucher: %s" % self.voucher
|
||||||
|
|
||||||
|
voucher = models.OneToOneField(inventory.Voucher)
|
||||||
|
|
||||||
|
|
||||||
|
# @python_2_unicode_compatible
|
||||||
|
class RoleFlag(object):
|
||||||
|
''' The condition is met because the active user has a particular Role.
|
||||||
|
This is for e.g. enabling Team tickets. '''
|
||||||
|
# TODO: implement RoleFlag
|
||||||
|
pass
|
172
registrasion/models/inventory.py
Normal file
172
registrasion/models/inventory.py
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
# Inventory Models
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Category(models.Model):
|
||||||
|
''' Registration product categories, used as logical groupings for Products
|
||||||
|
in registration forms.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name (str): The display name for the category.
|
||||||
|
|
||||||
|
description (str): Some explanatory text for the category. This is
|
||||||
|
displayed alongside the forms where your attendees choose their
|
||||||
|
items.
|
||||||
|
|
||||||
|
required (bool): Requires a user to select an item from this category
|
||||||
|
during initial registration. You can use this, e.g., for making
|
||||||
|
sure that the user has a ticket before they select whether they
|
||||||
|
want a t-shirt.
|
||||||
|
|
||||||
|
render_type (int): This is used to determine what sort of form the
|
||||||
|
attendee will be presented with when choosing Products from this
|
||||||
|
category. These may be either of the following:
|
||||||
|
|
||||||
|
``RENDER_TYPE_RADIO`` presents the Products in the Category as a
|
||||||
|
list of radio buttons. At most one item can be chosen at a time.
|
||||||
|
This works well when setting limit_per_user to 1.
|
||||||
|
|
||||||
|
``RENDER_TYPE_QUANTITY`` shows each Product next to an input field,
|
||||||
|
where the user can specify a quantity of each Product type. This is
|
||||||
|
useful for additional extras, like Dinner Tickets.
|
||||||
|
|
||||||
|
limit_per_user (Optional[int]): This restricts the number of items
|
||||||
|
from this Category that each attendee may claim. This extends
|
||||||
|
across multiple Invoices.
|
||||||
|
|
||||||
|
display_order (int): An ascending order for displaying the Categories
|
||||||
|
available. By convention, your Category for ticket types should
|
||||||
|
have the lowest display order.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
verbose_name = _("inventory - category")
|
||||||
|
verbose_name_plural = _("inventory - categories")
|
||||||
|
ordering = ("order", )
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
RENDER_TYPE_RADIO = 1
|
||||||
|
RENDER_TYPE_QUANTITY = 2
|
||||||
|
|
||||||
|
CATEGORY_RENDER_TYPES = [
|
||||||
|
(RENDER_TYPE_RADIO, _("Radio button")),
|
||||||
|
(RENDER_TYPE_QUANTITY, _("Quantity boxes")),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=65,
|
||||||
|
verbose_name=_("Name"),
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name=_("Description"),
|
||||||
|
)
|
||||||
|
limit_per_user = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Limit per user"),
|
||||||
|
help_text=_("The total number of items from this category one "
|
||||||
|
"attendee may purchase."),
|
||||||
|
)
|
||||||
|
required = models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
help_text=_("If enabled, a user must select an "
|
||||||
|
"item from this category."),
|
||||||
|
)
|
||||||
|
order = models.PositiveIntegerField(
|
||||||
|
verbose_name=("Display order"),
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
render_type = models.IntegerField(
|
||||||
|
choices=CATEGORY_RENDER_TYPES,
|
||||||
|
verbose_name=_("Render type"),
|
||||||
|
help_text=_("The registration form will render this category in this "
|
||||||
|
"style."),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Product(models.Model):
|
||||||
|
''' Registration products '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
verbose_name = _("inventory - product")
|
||||||
|
ordering = ("category__order", "order")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s" % (self.category.name, self.name)
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=65,
|
||||||
|
verbose_name=_("Name"),
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name=_("Description"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
category = models.ForeignKey(
|
||||||
|
Category,
|
||||||
|
verbose_name=_("Product category")
|
||||||
|
)
|
||||||
|
price = models.DecimalField(
|
||||||
|
max_digits=8,
|
||||||
|
decimal_places=2,
|
||||||
|
verbose_name=_("Price"),
|
||||||
|
)
|
||||||
|
limit_per_user = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Limit per user"),
|
||||||
|
)
|
||||||
|
reservation_duration = models.DurationField(
|
||||||
|
default=datetime.timedelta(hours=1),
|
||||||
|
verbose_name=_("Reservation duration"),
|
||||||
|
help_text=_("The length of time this product will be reserved before "
|
||||||
|
"it is released for someone else to purchase."),
|
||||||
|
)
|
||||||
|
order = models.PositiveIntegerField(
|
||||||
|
verbose_name=("Display order"),
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Voucher(models.Model):
|
||||||
|
''' Registration vouchers '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
# Vouchers reserve a cart for a fixed amount of time, so that
|
||||||
|
# items may be added without the voucher being swiped by someone else
|
||||||
|
RESERVATION_DURATION = datetime.timedelta(hours=1)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Voucher for %s" % self.recipient
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def normalise_code(cls, code):
|
||||||
|
return code.upper()
|
||||||
|
|
||||||
|
def save(self, *a, **k):
|
||||||
|
''' Normalise the voucher code to be uppercase '''
|
||||||
|
self.code = self.normalise_code(self.code)
|
||||||
|
super(Voucher, self).save(*a, **k)
|
||||||
|
|
||||||
|
recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
|
||||||
|
code = models.CharField(max_length=16,
|
||||||
|
unique=True,
|
||||||
|
verbose_name=_("Voucher code"))
|
||||||
|
limit = models.PositiveIntegerField(verbose_name=_("Voucher use limit"))
|
79
registrasion/models/people.py
Normal file
79
registrasion/models/people.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
from registrasion import util
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from model_utils.managers import InheritanceManager
|
||||||
|
|
||||||
|
|
||||||
|
# User models
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Attendee(models.Model):
|
||||||
|
''' Miscellaneous user-related data. '''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s" % self.user
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_instance(user):
|
||||||
|
''' Returns the instance of attendee for the given user, or creates
|
||||||
|
a new one. '''
|
||||||
|
try:
|
||||||
|
return Attendee.objects.get(user=user)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return Attendee.objects.create(user=user)
|
||||||
|
|
||||||
|
def save(self, *a, **k):
|
||||||
|
while not self.access_code:
|
||||||
|
access_code = util.generate_access_code()
|
||||||
|
if Attendee.objects.filter(access_code=access_code).count() == 0:
|
||||||
|
self.access_code = access_code
|
||||||
|
return super(Attendee, self).save(*a, **k)
|
||||||
|
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
|
# Badge/profile is linked
|
||||||
|
access_code = models.CharField(
|
||||||
|
max_length=6,
|
||||||
|
unique=True,
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
completed_registration = models.BooleanField(default=False)
|
||||||
|
guided_categories_complete = models.ManyToManyField("category")
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeProfileBase(models.Model):
|
||||||
|
''' Information for an attendee's badge and related preferences.
|
||||||
|
Subclass this in your Django site to ask for attendee information in your
|
||||||
|
registration progess.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "registrasion"
|
||||||
|
|
||||||
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def name_field(cls):
|
||||||
|
''' This is used to pre-fill the attendee's name from the
|
||||||
|
speaker profile. If it's None, that functionality is disabled. '''
|
||||||
|
return None
|
||||||
|
|
||||||
|
def invoice_recipient(self):
|
||||||
|
''' Returns a representation of this attendee profile for the purpose
|
||||||
|
of rendering to an invoice. Override in subclasses. '''
|
||||||
|
|
||||||
|
# Manual dispatch to subclass. Fleh.
|
||||||
|
slf = AttendeeProfileBase.objects.get_subclass(id=self.id)
|
||||||
|
# Actually compare the functions.
|
||||||
|
if type(slf).invoice_recipient != type(self).invoice_recipient:
|
||||||
|
return type(slf).invoice_recipient(slf)
|
||||||
|
|
||||||
|
# Return a default
|
||||||
|
return slf.attendee.user.username
|
||||||
|
|
||||||
|
attendee = models.OneToOneField(Attendee, on_delete=models.CASCADE)
|
|
@ -1,4 +1,5 @@
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import inventory
|
||||||
from registrasion.controllers.category import CategoryController
|
from registrasion.controllers.category import CategoryController
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
@ -19,7 +20,7 @@ def available_categories(context):
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def available_credit(context):
|
def available_credit(context):
|
||||||
''' Returns the amount of unclaimed credit available for this user. '''
|
''' Returns the amount of unclaimed credit available for this user. '''
|
||||||
notes = rego.CreditNote.unclaimed().filter(
|
notes = commerce.CreditNote.unclaimed().filter(
|
||||||
invoice__user=context.request.user,
|
invoice__user=context.request.user,
|
||||||
)
|
)
|
||||||
ret = notes.values("amount").aggregate(Sum("amount"))["amount__sum"] or 0
|
ret = notes.values("amount").aggregate(Sum("amount"))["amount__sum"] or 0
|
||||||
|
@ -29,7 +30,7 @@ def available_credit(context):
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def invoices(context):
|
def invoices(context):
|
||||||
''' Returns all of the invoices that this user has. '''
|
''' Returns all of the invoices that this user has. '''
|
||||||
return rego.Invoice.objects.filter(cart__user=context.request.user)
|
return commerce.Invoice.objects.filter(cart__user=context.request.user)
|
||||||
|
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
|
@ -37,7 +38,7 @@ def items_pending(context):
|
||||||
''' Returns all of the items that this user has in their current cart,
|
''' Returns all of the items that this user has in their current cart,
|
||||||
and is awaiting payment. '''
|
and is awaiting payment. '''
|
||||||
|
|
||||||
all_items = rego.ProductItem.objects.filter(
|
all_items = commerce.ProductItem.objects.filter(
|
||||||
cart__user=context.request.user,
|
cart__user=context.request.user,
|
||||||
cart__active=True,
|
cart__active=True,
|
||||||
).select_related(
|
).select_related(
|
||||||
|
@ -55,7 +56,7 @@ def items_purchased(context, category=None):
|
||||||
''' Returns all of the items that this user has purchased, optionally
|
''' Returns all of the items that this user has purchased, optionally
|
||||||
from the given category. '''
|
from the given category. '''
|
||||||
|
|
||||||
all_items = rego.ProductItem.objects.filter(
|
all_items = commerce.ProductItem.objects.filter(
|
||||||
cart__user=context.request.user,
|
cart__user=context.request.user,
|
||||||
cart__active=False,
|
cart__active=False,
|
||||||
cart__released=False,
|
cart__released=False,
|
||||||
|
@ -65,7 +66,7 @@ def items_purchased(context, category=None):
|
||||||
all_items = all_items.filter(product__category=category)
|
all_items = all_items.filter(product__category=category)
|
||||||
|
|
||||||
pq = all_items.values("product").annotate(quantity=Sum("quantity")).all()
|
pq = all_items.values("product").annotate(quantity=Sum("quantity")).all()
|
||||||
products = rego.Product.objects.all()
|
products = inventory.Product.objects.all()
|
||||||
out = []
|
out = []
|
||||||
for item in pq:
|
for item in pq:
|
||||||
prod = products.get(pk=item["product"])
|
prod = products.get(pk=item["product"])
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from registrasion.controllers.cart import CartController
|
from registrasion.controllers.cart import CartController
|
||||||
from registrasion.controllers.credit_note import CreditNoteController
|
from registrasion.controllers.credit_note import CreditNoteController
|
||||||
from registrasion.controllers.invoice import InvoiceController
|
from registrasion.controllers.invoice import InvoiceController
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class TestingCartController(CartController):
|
||||||
ValidationError if constraints are violated.'''
|
ValidationError if constraints are violated.'''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
product_item = rego.ProductItem.objects.get(
|
product_item = commerce.ProductItem.objects.get(
|
||||||
cart=self.cart,
|
cart=self.cart,
|
||||||
product=product)
|
product=product)
|
||||||
old_quantity = product_item.quantity
|
old_quantity = product_item.quantity
|
||||||
|
@ -41,7 +41,7 @@ class TestingInvoiceController(InvoiceController):
|
||||||
self.validate_allowed_to_pay()
|
self.validate_allowed_to_pay()
|
||||||
|
|
||||||
''' Adds a payment '''
|
''' Adds a payment '''
|
||||||
rego.ManualPayment.objects.create(
|
commerce.ManualPayment.objects.create(
|
||||||
invoice=self.invoice,
|
invoice=self.invoice,
|
||||||
reference=reference,
|
reference=reference,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
|
@ -53,7 +53,7 @@ class TestingInvoiceController(InvoiceController):
|
||||||
class TestingCreditNoteController(CreditNoteController):
|
class TestingCreditNoteController(CreditNoteController):
|
||||||
|
|
||||||
def refund(self):
|
def refund(self):
|
||||||
rego.CreditNoteRefund.objects.create(
|
commerce.CreditNoteRefund.objects.create(
|
||||||
parent=self.credit_note,
|
parent=self.credit_note,
|
||||||
reference="Whoops."
|
reference="Whoops."
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,10 @@ 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
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import conditions
|
||||||
|
from registrasion.models import inventory
|
||||||
|
from registrasion.models import people
|
||||||
from registrasion.controllers.product import ProductController
|
from registrasion.controllers.product import ProductController
|
||||||
|
|
||||||
from controller_helpers import TestingCartController
|
from controller_helpers import TestingCartController
|
||||||
|
@ -36,24 +39,28 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
||||||
email='test2@example.com',
|
email='test2@example.com',
|
||||||
password='top_secret')
|
password='top_secret')
|
||||||
|
|
||||||
attendee1 = rego.Attendee.get_instance(cls.USER_1)
|
attendee1 = people.Attendee.get_instance(cls.USER_1)
|
||||||
attendee1.save()
|
attendee1.save()
|
||||||
profile1 = rego.AttendeeProfileBase.objects.create(attendee=attendee1)
|
profile1 = people.AttendeeProfileBase.objects.create(
|
||||||
|
attendee=attendee1,
|
||||||
|
)
|
||||||
profile1.save()
|
profile1.save()
|
||||||
attendee2 = rego.Attendee.get_instance(cls.USER_2)
|
attendee2 = people.Attendee.get_instance(cls.USER_2)
|
||||||
attendee2.save()
|
attendee2.save()
|
||||||
profile2 = rego.AttendeeProfileBase.objects.create(attendee=attendee2)
|
profile2 = people.AttendeeProfileBase.objects.create(
|
||||||
|
attendee=attendee2,
|
||||||
|
)
|
||||||
profile2.save()
|
profile2.save()
|
||||||
|
|
||||||
cls.RESERVATION = datetime.timedelta(hours=1)
|
cls.RESERVATION = datetime.timedelta(hours=1)
|
||||||
|
|
||||||
cls.categories = []
|
cls.categories = []
|
||||||
for i in xrange(2):
|
for i in xrange(2):
|
||||||
cat = rego.Category.objects.create(
|
cat = inventory.Category.objects.create(
|
||||||
name="Category " + str(i + 1),
|
name="Category " + str(i + 1),
|
||||||
description="This is a test category",
|
description="This is a test category",
|
||||||
order=i,
|
order=i,
|
||||||
render_type=rego.Category.RENDER_TYPE_RADIO,
|
render_type=inventory.Category.RENDER_TYPE_RADIO,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
cat.save()
|
cat.save()
|
||||||
|
@ -64,7 +71,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
||||||
|
|
||||||
cls.products = []
|
cls.products = []
|
||||||
for i in xrange(4):
|
for i in xrange(4):
|
||||||
prod = rego.Product.objects.create(
|
prod = inventory.Product.objects.create(
|
||||||
name="Product " + str(i + 1),
|
name="Product " + str(i + 1),
|
||||||
description="This is a test product.",
|
description="This is a test product.",
|
||||||
category=cls.categories[i / 2], # 2 products per category
|
category=cls.categories[i / 2], # 2 products per category
|
||||||
|
@ -95,9 +102,9 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
||||||
|
|
||||||
@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):
|
||||||
limit_ceiling = rego.TimeOrStockLimitFlag.objects.create(
|
limit_ceiling = conditions.TimeOrStockLimitFlag.objects.create(
|
||||||
description=name,
|
description=name,
|
||||||
condition=rego.FlagBase.DISABLE_IF_FALSE,
|
condition=conditions.FlagBase.DISABLE_IF_FALSE,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time
|
end_time=end_time
|
||||||
|
@ -109,9 +116,9 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_category_ceiling(
|
def make_category_ceiling(
|
||||||
cls, name, limit=None, start_time=None, end_time=None):
|
cls, name, limit=None, start_time=None, end_time=None):
|
||||||
limit_ceiling = rego.TimeOrStockLimitFlag.objects.create(
|
limit_ceiling = conditions.TimeOrStockLimitFlag.objects.create(
|
||||||
description=name,
|
description=name,
|
||||||
condition=rego.FlagBase.DISABLE_IF_FALSE,
|
condition=conditions.FlagBase.DISABLE_IF_FALSE,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time
|
end_time=end_time
|
||||||
|
@ -124,14 +131,14 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
||||||
def make_discount_ceiling(
|
def make_discount_ceiling(
|
||||||
cls, name, limit=None, start_time=None, end_time=None,
|
cls, name, limit=None, start_time=None, end_time=None,
|
||||||
percentage=100):
|
percentage=100):
|
||||||
limit_ceiling = rego.TimeOrStockLimitDiscount.objects.create(
|
limit_ceiling = conditions.TimeOrStockLimitDiscount.objects.create(
|
||||||
description=name,
|
description=name,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
limit_ceiling.save()
|
limit_ceiling.save()
|
||||||
rego.DiscountForProduct.objects.create(
|
conditions.DiscountForProduct.objects.create(
|
||||||
discount=limit_ceiling,
|
discount=limit_ceiling,
|
||||||
product=cls.PROD_1,
|
product=cls.PROD_1,
|
||||||
percentage=percentage,
|
percentage=percentage,
|
||||||
|
@ -140,7 +147,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def new_voucher(self, code="VOUCHER", limit=1):
|
def new_voucher(self, code="VOUCHER", limit=1):
|
||||||
voucher = rego.Voucher.objects.create(
|
voucher = inventory.Voucher.objects.create(
|
||||||
recipient="Voucher recipient",
|
recipient="Voucher recipient",
|
||||||
code=code,
|
code=code,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
|
@ -176,7 +183,7 @@ class BasicCartTests(RegistrationCartTestCase):
|
||||||
current_cart.add_to_cart(self.PROD_1, 1)
|
current_cart.add_to_cart(self.PROD_1, 1)
|
||||||
|
|
||||||
# Count of products for a given user should be collapsed.
|
# Count of products for a given user should be collapsed.
|
||||||
items = rego.ProductItem.objects.filter(
|
items = commerce.ProductItem.objects.filter(
|
||||||
cart=current_cart.cart,
|
cart=current_cart.cart,
|
||||||
product=self.PROD_1)
|
product=self.PROD_1)
|
||||||
self.assertEqual(1, len(items))
|
self.assertEqual(1, len(items))
|
||||||
|
@ -187,7 +194,7 @@ class BasicCartTests(RegistrationCartTestCase):
|
||||||
current_cart = TestingCartController.for_user(self.USER_1)
|
current_cart = TestingCartController.for_user(self.USER_1)
|
||||||
|
|
||||||
def get_item():
|
def get_item():
|
||||||
return rego.ProductItem.objects.get(
|
return commerce.ProductItem.objects.get(
|
||||||
cart=current_cart.cart,
|
cart=current_cart.cart,
|
||||||
product=self.PROD_1)
|
product=self.PROD_1)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError
|
||||||
from controller_helpers import TestingCartController
|
from controller_helpers import TestingCartController
|
||||||
from test_cart import RegistrationCartTestCase
|
from test_cart import RegistrationCartTestCase
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import conditions
|
||||||
|
|
||||||
UTC = pytz.timezone('UTC')
|
UTC = pytz.timezone('UTC')
|
||||||
|
|
||||||
|
@ -155,12 +155,12 @@ class CeilingsTestCases(RegistrationCartTestCase):
|
||||||
self.make_discount_ceiling("Limit ceiling", limit=1, percentage=50)
|
self.make_discount_ceiling("Limit ceiling", limit=1, percentage=50)
|
||||||
voucher = self.new_voucher(code="VOUCHER")
|
voucher = self.new_voucher(code="VOUCHER")
|
||||||
|
|
||||||
discount = rego.VoucherDiscount.objects.create(
|
discount = conditions.VoucherDiscount.objects.create(
|
||||||
description="VOUCHER RECIPIENT",
|
description="VOUCHER RECIPIENT",
|
||||||
voucher=voucher,
|
voucher=voucher,
|
||||||
)
|
)
|
||||||
discount.save()
|
discount.save()
|
||||||
rego.DiscountForProduct.objects.create(
|
conditions.DiscountForProduct.objects.create(
|
||||||
discount=discount,
|
discount=discount,
|
||||||
product=self.PROD_1,
|
product=self.PROD_1,
|
||||||
percentage=100,
|
percentage=100,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import pytz
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import conditions
|
||||||
from registrasion.controllers import discount
|
from registrasion.controllers import discount
|
||||||
from controller_helpers import TestingCartController
|
from controller_helpers import TestingCartController
|
||||||
|
|
||||||
|
@ -19,13 +19,13 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
amount=Decimal(100),
|
amount=Decimal(100),
|
||||||
quantity=2,
|
quantity=2,
|
||||||
):
|
):
|
||||||
discount = rego.IncludedProductDiscount.objects.create(
|
discount = conditions.IncludedProductDiscount.objects.create(
|
||||||
description="PROD_1 includes PROD_2 " + str(amount) + "%",
|
description="PROD_1 includes PROD_2 " + str(amount) + "%",
|
||||||
)
|
)
|
||||||
discount.save()
|
discount.save()
|
||||||
discount.enabling_products.add(cls.PROD_1)
|
discount.enabling_products.add(cls.PROD_1)
|
||||||
discount.save()
|
discount.save()
|
||||||
rego.DiscountForProduct.objects.create(
|
conditions.DiscountForProduct.objects.create(
|
||||||
discount=discount,
|
discount=discount,
|
||||||
product=cls.PROD_2,
|
product=cls.PROD_2,
|
||||||
percentage=amount,
|
percentage=amount,
|
||||||
|
@ -39,13 +39,13 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
amount=Decimal(100),
|
amount=Decimal(100),
|
||||||
quantity=2,
|
quantity=2,
|
||||||
):
|
):
|
||||||
discount = rego.IncludedProductDiscount.objects.create(
|
discount = conditions.IncludedProductDiscount.objects.create(
|
||||||
description="PROD_1 includes CAT_2 " + str(amount) + "%",
|
description="PROD_1 includes CAT_2 " + str(amount) + "%",
|
||||||
)
|
)
|
||||||
discount.save()
|
discount.save()
|
||||||
discount.enabling_products.add(cls.PROD_1)
|
discount.enabling_products.add(cls.PROD_1)
|
||||||
discount.save()
|
discount.save()
|
||||||
rego.DiscountForCategory.objects.create(
|
conditions.DiscountForCategory.objects.create(
|
||||||
discount=discount,
|
discount=discount,
|
||||||
category=cls.CAT_2,
|
category=cls.CAT_2,
|
||||||
percentage=amount,
|
percentage=amount,
|
||||||
|
@ -59,20 +59,20 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
amount=Decimal(100),
|
amount=Decimal(100),
|
||||||
quantity=2,
|
quantity=2,
|
||||||
):
|
):
|
||||||
discount = rego.IncludedProductDiscount.objects.create(
|
discount = conditions.IncludedProductDiscount.objects.create(
|
||||||
description="PROD_1 includes PROD_3 and PROD_4 " +
|
description="PROD_1 includes PROD_3 and PROD_4 " +
|
||||||
str(amount) + "%",
|
str(amount) + "%",
|
||||||
)
|
)
|
||||||
discount.save()
|
discount.save()
|
||||||
discount.enabling_products.add(cls.PROD_1)
|
discount.enabling_products.add(cls.PROD_1)
|
||||||
discount.save()
|
discount.save()
|
||||||
rego.DiscountForProduct.objects.create(
|
conditions.DiscountForProduct.objects.create(
|
||||||
discount=discount,
|
discount=discount,
|
||||||
product=cls.PROD_3,
|
product=cls.PROD_3,
|
||||||
percentage=amount,
|
percentage=amount,
|
||||||
quantity=quantity,
|
quantity=quantity,
|
||||||
).save()
|
).save()
|
||||||
rego.DiscountForProduct.objects.create(
|
conditions.DiscountForProduct.objects.create(
|
||||||
discount=discount,
|
discount=discount,
|
||||||
product=cls.PROD_4,
|
product=cls.PROD_4,
|
||||||
percentage=amount,
|
percentage=amount,
|
||||||
|
|
|
@ -2,7 +2,8 @@ import pytz
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import conditions
|
||||||
from registrasion.controllers.category import CategoryController
|
from registrasion.controllers.category import CategoryController
|
||||||
from controller_helpers import TestingCartController
|
from controller_helpers import TestingCartController
|
||||||
from registrasion.controllers.product import ProductController
|
from registrasion.controllers.product import ProductController
|
||||||
|
@ -15,10 +16,10 @@ UTC = pytz.timezone('UTC')
|
||||||
class FlagTestCases(RegistrationCartTestCase):
|
class FlagTestCases(RegistrationCartTestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_product_flag(cls, condition=rego.FlagBase.ENABLE_IF_TRUE):
|
def add_product_flag(cls, condition=conditions.FlagBase.ENABLE_IF_TRUE):
|
||||||
''' Adds a product flag condition: adding PROD_1 to a cart is
|
''' Adds a product flag condition: adding PROD_1 to a cart is
|
||||||
predicated on adding PROD_2 beforehand. '''
|
predicated on adding PROD_2 beforehand. '''
|
||||||
flag = rego.ProductFlag.objects.create(
|
flag = conditions.ProductFlag.objects.create(
|
||||||
description="Product condition",
|
description="Product condition",
|
||||||
condition=condition,
|
condition=condition,
|
||||||
)
|
)
|
||||||
|
@ -28,10 +29,13 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
flag.save()
|
flag.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_product_flag_on_category(cls, condition=rego.FlagBase.ENABLE_IF_TRUE):
|
def add_product_flag_on_category(
|
||||||
|
cls,
|
||||||
|
condition=conditions.FlagBase.ENABLE_IF_TRUE,
|
||||||
|
):
|
||||||
''' Adds a product flag condition that operates on a category:
|
''' Adds a product flag condition that operates on a category:
|
||||||
adding an item from CAT_1 is predicated on adding PROD_3 beforehand '''
|
adding an item from CAT_1 is predicated on adding PROD_3 beforehand '''
|
||||||
flag = rego.ProductFlag.objects.create(
|
flag = conditions.ProductFlag.objects.create(
|
||||||
description="Product condition",
|
description="Product condition",
|
||||||
condition=condition,
|
condition=condition,
|
||||||
)
|
)
|
||||||
|
@ -40,10 +44,10 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
flag.enabling_products.add(cls.PROD_3)
|
flag.enabling_products.add(cls.PROD_3)
|
||||||
flag.save()
|
flag.save()
|
||||||
|
|
||||||
def add_category_flag(cls, condition=rego.FlagBase.ENABLE_IF_TRUE):
|
def add_category_flag(cls, condition=conditions.FlagBase.ENABLE_IF_TRUE):
|
||||||
''' Adds a category flag condition: adding PROD_1 to a cart is
|
''' Adds a category flag condition: adding PROD_1 to a cart is
|
||||||
predicated on adding an item from CAT_2 beforehand.'''
|
predicated on adding an item from CAT_2 beforehand.'''
|
||||||
flag = rego.CategoryFlag.objects.create(
|
flag = conditions.CategoryFlag.objects.create(
|
||||||
description="Category condition",
|
description="Category condition",
|
||||||
condition=condition,
|
condition=condition,
|
||||||
enabling_category=cls.CAT_2,
|
enabling_category=cls.CAT_2,
|
||||||
|
@ -131,8 +135,8 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
cart_2.add_to_cart(self.PROD_1, 1)
|
cart_2.add_to_cart(self.PROD_1, 1)
|
||||||
|
|
||||||
def test_multiple_dif_conditions(self):
|
def test_multiple_dif_conditions(self):
|
||||||
self.add_product_flag(condition=rego.FlagBase.DISABLE_IF_FALSE)
|
self.add_product_flag(condition=conditions.FlagBase.DISABLE_IF_FALSE)
|
||||||
self.add_category_flag(condition=rego.FlagBase.DISABLE_IF_FALSE)
|
self.add_category_flag(condition=conditions.FlagBase.DISABLE_IF_FALSE)
|
||||||
|
|
||||||
cart_1 = TestingCartController.for_user(self.USER_1)
|
cart_1 = TestingCartController.for_user(self.USER_1)
|
||||||
# Cannot add PROD_1 until both conditions are met
|
# Cannot add PROD_1 until both conditions are met
|
||||||
|
@ -145,8 +149,8 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
cart_1.add_to_cart(self.PROD_1, 1)
|
cart_1.add_to_cart(self.PROD_1, 1)
|
||||||
|
|
||||||
def test_eit_and_dif_conditions_work_together(self):
|
def test_eit_and_dif_conditions_work_together(self):
|
||||||
self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
self.add_product_flag(condition=conditions.FlagBase.ENABLE_IF_TRUE)
|
||||||
self.add_category_flag(condition=rego.FlagBase.DISABLE_IF_FALSE)
|
self.add_category_flag(condition=conditions.FlagBase.DISABLE_IF_FALSE)
|
||||||
|
|
||||||
cart_1 = TestingCartController.for_user(self.USER_1)
|
cart_1 = TestingCartController.for_user(self.USER_1)
|
||||||
# Cannot add PROD_1 until both conditions are met
|
# Cannot add PROD_1 until both conditions are met
|
||||||
|
@ -200,7 +204,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
self.assertTrue(self.PROD_4 in prods)
|
self.assertTrue(self.PROD_4 in prods)
|
||||||
|
|
||||||
def test_available_products_on_category_works_when_condition_not_met(self):
|
def test_available_products_on_category_works_when_condition_not_met(self):
|
||||||
self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
self.add_product_flag(condition=conditions.FlagBase.ENABLE_IF_TRUE)
|
||||||
|
|
||||||
prods = ProductController.available_products(
|
prods = ProductController.available_products(
|
||||||
self.USER_1,
|
self.USER_1,
|
||||||
|
@ -211,7 +215,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
self.assertTrue(self.PROD_2 in prods)
|
self.assertTrue(self.PROD_2 in prods)
|
||||||
|
|
||||||
def test_available_products_on_category_works_when_condition_is_met(self):
|
def test_available_products_on_category_works_when_condition_is_met(self):
|
||||||
self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
self.add_product_flag(condition=conditions.FlagBase.ENABLE_IF_TRUE)
|
||||||
|
|
||||||
cart_1 = TestingCartController.for_user(self.USER_1)
|
cart_1 = TestingCartController.for_user(self.USER_1)
|
||||||
cart_1.add_to_cart(self.PROD_2, 1)
|
cart_1.add_to_cart(self.PROD_2, 1)
|
||||||
|
@ -225,7 +229,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
self.assertTrue(self.PROD_2 in prods)
|
self.assertTrue(self.PROD_2 in prods)
|
||||||
|
|
||||||
def test_available_products_on_products_works_when_condition_not_met(self):
|
def test_available_products_on_products_works_when_condition_not_met(self):
|
||||||
self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
self.add_product_flag(condition=conditions.FlagBase.ENABLE_IF_TRUE)
|
||||||
|
|
||||||
prods = ProductController.available_products(
|
prods = ProductController.available_products(
|
||||||
self.USER_1,
|
self.USER_1,
|
||||||
|
@ -236,7 +240,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
self.assertTrue(self.PROD_2 in prods)
|
self.assertTrue(self.PROD_2 in prods)
|
||||||
|
|
||||||
def test_available_products_on_products_works_when_condition_is_met(self):
|
def test_available_products_on_products_works_when_condition_is_met(self):
|
||||||
self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
self.add_product_flag(condition=conditions.FlagBase.ENABLE_IF_TRUE)
|
||||||
|
|
||||||
cart_1 = TestingCartController.for_user(self.USER_1)
|
cart_1 = TestingCartController.for_user(self.USER_1)
|
||||||
cart_1.add_to_cart(self.PROD_2, 1)
|
cart_1.add_to_cart(self.PROD_2, 1)
|
||||||
|
@ -250,7 +254,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
self.assertTrue(self.PROD_2 in prods)
|
self.assertTrue(self.PROD_2 in prods)
|
||||||
|
|
||||||
def test_category_flag_fails_if_cart_refunded(self):
|
def test_category_flag_fails_if_cart_refunded(self):
|
||||||
self.add_category_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
self.add_category_flag(condition=conditions.FlagBase.ENABLE_IF_TRUE)
|
||||||
|
|
||||||
cart = TestingCartController.for_user(self.USER_1)
|
cart = TestingCartController.for_user(self.USER_1)
|
||||||
cart.add_to_cart(self.PROD_3, 1)
|
cart.add_to_cart(self.PROD_3, 1)
|
||||||
|
@ -268,7 +272,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
cart_2.set_quantity(self.PROD_1, 1)
|
cart_2.set_quantity(self.PROD_1, 1)
|
||||||
|
|
||||||
def test_product_flag_fails_if_cart_refunded(self):
|
def test_product_flag_fails_if_cart_refunded(self):
|
||||||
self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
self.add_product_flag(condition=conditions.FlagBase.ENABLE_IF_TRUE)
|
||||||
|
|
||||||
cart = TestingCartController.for_user(self.USER_1)
|
cart = TestingCartController.for_user(self.USER_1)
|
||||||
cart.add_to_cart(self.PROD_2, 1)
|
cart.add_to_cart(self.PROD_2, 1)
|
||||||
|
@ -286,7 +290,9 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
cart_2.set_quantity(self.PROD_1, 1)
|
cart_2.set_quantity(self.PROD_1, 1)
|
||||||
|
|
||||||
def test_available_categories(self):
|
def test_available_categories(self):
|
||||||
self.add_product_flag_on_category(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
self.add_product_flag_on_category(
|
||||||
|
condition=conditions.FlagBase.ENABLE_IF_TRUE,
|
||||||
|
)
|
||||||
|
|
||||||
cart_1 = TestingCartController.for_user(self.USER_1)
|
cart_1 = TestingCartController.for_user(self.USER_1)
|
||||||
|
|
||||||
|
@ -307,7 +313,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
self.assertTrue(self.CAT_2 in cats)
|
self.assertTrue(self.CAT_2 in cats)
|
||||||
|
|
||||||
def test_validate_cart_when_flags_become_unmet(self):
|
def test_validate_cart_when_flags_become_unmet(self):
|
||||||
self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
self.add_product_flag(condition=conditions.FlagBase.ENABLE_IF_TRUE)
|
||||||
|
|
||||||
cart = TestingCartController.for_user(self.USER_1)
|
cart = TestingCartController.for_user(self.USER_1)
|
||||||
cart.add_to_cart(self.PROD_2, 1)
|
cart.add_to_cart(self.PROD_2, 1)
|
||||||
|
@ -332,7 +338,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
cart.validate_cart()
|
cart.validate_cart()
|
||||||
|
|
||||||
# Should keep PROD_2 in the cart
|
# Should keep PROD_2 in the cart
|
||||||
items = rego.ProductItem.objects.filter(cart=cart.cart)
|
items = commerce.ProductItem.objects.filter(cart=cart.cart)
|
||||||
self.assertFalse([i for i in items if i.product == self.PROD_1])
|
self.assertFalse([i for i in items if i.product == self.PROD_1])
|
||||||
|
|
||||||
def test_fix_simple_errors_does_not_remove_limited_items(self):
|
def test_fix_simple_errors_does_not_remove_limited_items(self):
|
||||||
|
@ -348,5 +354,5 @@ class FlagTestCases(RegistrationCartTestCase):
|
||||||
|
|
||||||
# Should keep PROD_2 in the cart
|
# Should keep PROD_2 in the cart
|
||||||
# and also PROD_1, which is now exhausted for user.
|
# and also PROD_1, which is now exhausted for user.
|
||||||
items = rego.ProductItem.objects.filter(cart=cart.cart)
|
items = commerce.ProductItem.objects.filter(cart=cart.cart)
|
||||||
self.assertTrue([i for i in items if i.product == self.PROD_1])
|
self.assertTrue([i for i in items if i.product == self.PROD_1])
|
||||||
|
|
|
@ -4,7 +4,9 @@ import pytz
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import conditions
|
||||||
|
from registrasion.models import inventory
|
||||||
from controller_helpers import TestingCartController
|
from controller_helpers import TestingCartController
|
||||||
from controller_helpers import TestingCreditNoteController
|
from controller_helpers import TestingCreditNoteController
|
||||||
from controller_helpers import TestingInvoiceController
|
from controller_helpers import TestingInvoiceController
|
||||||
|
@ -23,7 +25,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
current_cart.add_to_cart(self.PROD_1, 1)
|
current_cart.add_to_cart(self.PROD_1, 1)
|
||||||
invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
|
invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
|
||||||
# That invoice should have a single line item
|
# That invoice should have a single line item
|
||||||
line_items = rego.LineItem.objects.filter(invoice=invoice_1.invoice)
|
line_items = commerce.LineItem.objects.filter(
|
||||||
|
invoice=invoice_1.invoice,
|
||||||
|
)
|
||||||
self.assertEqual(1, len(line_items))
|
self.assertEqual(1, len(line_items))
|
||||||
# That invoice should have a value equal to cost of PROD_1
|
# That invoice should have a value equal to cost of PROD_1
|
||||||
self.assertEqual(self.PROD_1.price, invoice_1.invoice.value)
|
self.assertEqual(self.PROD_1.price, invoice_1.invoice.value)
|
||||||
|
@ -34,13 +38,15 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
self.assertNotEqual(invoice_1.invoice, invoice_2.invoice)
|
self.assertNotEqual(invoice_1.invoice, invoice_2.invoice)
|
||||||
|
|
||||||
# The old invoice should automatically be voided
|
# The old invoice should automatically be voided
|
||||||
invoice_1_new = rego.Invoice.objects.get(pk=invoice_1.invoice.id)
|
invoice_1_new = commerce.Invoice.objects.get(pk=invoice_1.invoice.id)
|
||||||
invoice_2_new = rego.Invoice.objects.get(pk=invoice_2.invoice.id)
|
invoice_2_new = commerce.Invoice.objects.get(pk=invoice_2.invoice.id)
|
||||||
self.assertTrue(invoice_1_new.is_void)
|
self.assertTrue(invoice_1_new.is_void)
|
||||||
self.assertFalse(invoice_2_new.is_void)
|
self.assertFalse(invoice_2_new.is_void)
|
||||||
|
|
||||||
# Invoice should have two line items
|
# Invoice should have two line items
|
||||||
line_items = rego.LineItem.objects.filter(invoice=invoice_2.invoice)
|
line_items = commerce.LineItem.objects.filter(
|
||||||
|
invoice=invoice_2.invoice,
|
||||||
|
)
|
||||||
self.assertEqual(2, len(line_items))
|
self.assertEqual(2, len(line_items))
|
||||||
# Invoice should have a value equal to cost of PROD_1 and PROD_2
|
# Invoice should have a value equal to cost of PROD_1 and PROD_2
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -79,16 +85,16 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
self.assertNotEqual(current_cart.cart, new_cart.cart)
|
self.assertNotEqual(current_cart.cart, new_cart.cart)
|
||||||
|
|
||||||
def test_invoice_includes_discounts(self):
|
def test_invoice_includes_discounts(self):
|
||||||
voucher = rego.Voucher.objects.create(
|
voucher = inventory.Voucher.objects.create(
|
||||||
recipient="Voucher recipient",
|
recipient="Voucher recipient",
|
||||||
code="VOUCHER",
|
code="VOUCHER",
|
||||||
limit=1
|
limit=1
|
||||||
)
|
)
|
||||||
discount = rego.VoucherDiscount.objects.create(
|
discount = conditions.VoucherDiscount.objects.create(
|
||||||
description="VOUCHER RECIPIENT",
|
description="VOUCHER RECIPIENT",
|
||||||
voucher=voucher,
|
voucher=voucher,
|
||||||
)
|
)
|
||||||
rego.DiscountForProduct.objects.create(
|
conditions.DiscountForProduct.objects.create(
|
||||||
discount=discount,
|
discount=discount,
|
||||||
product=self.PROD_1,
|
product=self.PROD_1,
|
||||||
percentage=Decimal(50),
|
percentage=Decimal(50),
|
||||||
|
@ -103,7 +109,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
|
invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
|
||||||
|
|
||||||
# That invoice should have two line items
|
# That invoice should have two line items
|
||||||
line_items = rego.LineItem.objects.filter(invoice=invoice_1.invoice)
|
line_items = commerce.LineItem.objects.filter(
|
||||||
|
invoice=invoice_1.invoice,
|
||||||
|
)
|
||||||
self.assertEqual(2, len(line_items))
|
self.assertEqual(2, len(line_items))
|
||||||
# That invoice should have a value equal to 50% of the cost of PROD_1
|
# That invoice should have a value equal to 50% of the cost of PROD_1
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -111,16 +119,16 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
invoice_1.invoice.value)
|
invoice_1.invoice.value)
|
||||||
|
|
||||||
def test_zero_value_invoice_is_automatically_paid(self):
|
def test_zero_value_invoice_is_automatically_paid(self):
|
||||||
voucher = rego.Voucher.objects.create(
|
voucher = inventory.Voucher.objects.create(
|
||||||
recipient="Voucher recipient",
|
recipient="Voucher recipient",
|
||||||
code="VOUCHER",
|
code="VOUCHER",
|
||||||
limit=1
|
limit=1
|
||||||
)
|
)
|
||||||
discount = rego.VoucherDiscount.objects.create(
|
discount = conditions.VoucherDiscount.objects.create(
|
||||||
description="VOUCHER RECIPIENT",
|
description="VOUCHER RECIPIENT",
|
||||||
voucher=voucher,
|
voucher=voucher,
|
||||||
)
|
)
|
||||||
rego.DiscountForProduct.objects.create(
|
conditions.DiscountForProduct.objects.create(
|
||||||
discount=discount,
|
discount=discount,
|
||||||
product=self.PROD_1,
|
product=self.PROD_1,
|
||||||
percentage=Decimal(100),
|
percentage=Decimal(100),
|
||||||
|
@ -239,7 +247,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
self.assertTrue(invoice.invoice.is_paid)
|
self.assertTrue(invoice.invoice.is_paid)
|
||||||
|
|
||||||
# There should be a credit note generated out of the invoice.
|
# There should be a credit note generated out of the invoice.
|
||||||
credit_notes = rego.CreditNote.objects.filter(invoice=invoice.invoice)
|
credit_notes = commerce.CreditNote.objects.filter(
|
||||||
|
invoice=invoice.invoice,
|
||||||
|
)
|
||||||
self.assertEqual(1, credit_notes.count())
|
self.assertEqual(1, credit_notes.count())
|
||||||
self.assertEqual(to_pay - invoice.invoice.value, credit_notes[0].value)
|
self.assertEqual(to_pay - invoice.invoice.value, credit_notes[0].value)
|
||||||
|
|
||||||
|
@ -257,7 +267,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
self.assertTrue(invoice.invoice.is_paid)
|
self.assertTrue(invoice.invoice.is_paid)
|
||||||
|
|
||||||
# There should be no credit notes
|
# There should be no credit notes
|
||||||
credit_notes = rego.CreditNote.objects.filter(invoice=invoice.invoice)
|
credit_notes = commerce.CreditNote.objects.filter(
|
||||||
|
invoice=invoice.invoice,
|
||||||
|
)
|
||||||
self.assertEqual(0, credit_notes.count())
|
self.assertEqual(0, credit_notes.count())
|
||||||
|
|
||||||
def test_refund_partially_paid_invoice_generates_correct_credit_note(self):
|
def test_refund_partially_paid_invoice_generates_correct_credit_note(self):
|
||||||
|
@ -276,7 +288,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
self.assertTrue(invoice.invoice.is_void)
|
self.assertTrue(invoice.invoice.is_void)
|
||||||
|
|
||||||
# There should be a credit note generated out of the invoice.
|
# There should be a credit note generated out of the invoice.
|
||||||
credit_notes = rego.CreditNote.objects.filter(invoice=invoice.invoice)
|
credit_notes = commerce.CreditNote.objects.filter(
|
||||||
|
invoice=invoice.invoice,
|
||||||
|
)
|
||||||
self.assertEqual(1, credit_notes.count())
|
self.assertEqual(1, credit_notes.count())
|
||||||
self.assertEqual(to_pay, credit_notes[0].value)
|
self.assertEqual(to_pay, credit_notes[0].value)
|
||||||
|
|
||||||
|
@ -297,7 +311,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
self.assertTrue(invoice.invoice.is_refunded)
|
self.assertTrue(invoice.invoice.is_refunded)
|
||||||
|
|
||||||
# There should be a credit note generated out of the invoice.
|
# There should be a credit note generated out of the invoice.
|
||||||
credit_notes = rego.CreditNote.objects.filter(invoice=invoice.invoice)
|
credit_notes = commerce.CreditNote.objects.filter(
|
||||||
|
invoice=invoice.invoice,
|
||||||
|
)
|
||||||
self.assertEqual(1, credit_notes.count())
|
self.assertEqual(1, credit_notes.count())
|
||||||
self.assertEqual(to_pay, credit_notes[0].value)
|
self.assertEqual(to_pay, credit_notes[0].value)
|
||||||
|
|
||||||
|
@ -314,11 +330,11 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
invoice.refund()
|
invoice.refund()
|
||||||
|
|
||||||
# There should be one credit note generated out of the invoice.
|
# There should be one credit note generated out of the invoice.
|
||||||
credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice)
|
credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice)
|
||||||
cn = TestingCreditNoteController(credit_note)
|
cn = TestingCreditNoteController(credit_note)
|
||||||
|
|
||||||
# That credit note should be in the unclaimed pile
|
# That credit note should be in the unclaimed pile
|
||||||
self.assertEquals(1, rego.CreditNote.unclaimed().count())
|
self.assertEquals(1, commerce.CreditNote.unclaimed().count())
|
||||||
|
|
||||||
# Create a new (identical) cart with invoice
|
# Create a new (identical) cart with invoice
|
||||||
cart = TestingCartController.for_user(self.USER_1)
|
cart = TestingCartController.for_user(self.USER_1)
|
||||||
|
@ -330,7 +346,7 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
self.assertTrue(invoice2.invoice.is_paid)
|
self.assertTrue(invoice2.invoice.is_paid)
|
||||||
|
|
||||||
# That invoice should not show up as unclaimed any more
|
# That invoice should not show up as unclaimed any more
|
||||||
self.assertEquals(0, rego.CreditNote.unclaimed().count())
|
self.assertEquals(0, commerce.CreditNote.unclaimed().count())
|
||||||
|
|
||||||
def test_apply_credit_note_generates_new_credit_note_if_overpaying(self):
|
def test_apply_credit_note_generates_new_credit_note_if_overpaying(self):
|
||||||
cart = TestingCartController.for_user(self.USER_1)
|
cart = TestingCartController.for_user(self.USER_1)
|
||||||
|
@ -345,10 +361,10 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
invoice.refund()
|
invoice.refund()
|
||||||
|
|
||||||
# There should be one credit note generated out of the invoice.
|
# There should be one credit note generated out of the invoice.
|
||||||
credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice)
|
credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice)
|
||||||
cn = TestingCreditNoteController(credit_note)
|
cn = TestingCreditNoteController(credit_note)
|
||||||
|
|
||||||
self.assertEquals(1, rego.CreditNote.unclaimed().count())
|
self.assertEquals(1, commerce.CreditNote.unclaimed().count())
|
||||||
|
|
||||||
# Create a new cart (of half value of inv 1) and get invoice
|
# Create a new cart (of half value of inv 1) and get invoice
|
||||||
cart = TestingCartController.for_user(self.USER_1)
|
cart = TestingCartController.for_user(self.USER_1)
|
||||||
|
@ -361,9 +377,11 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
|
|
||||||
# We generated a new credit note, and spent the old one,
|
# We generated a new credit note, and spent the old one,
|
||||||
# unclaimed should still be 1.
|
# unclaimed should still be 1.
|
||||||
self.assertEquals(1, rego.CreditNote.unclaimed().count())
|
self.assertEquals(1, commerce.CreditNote.unclaimed().count())
|
||||||
|
|
||||||
credit_note2 = rego.CreditNote.objects.get(invoice=invoice2.invoice)
|
credit_note2 = commerce.CreditNote.objects.get(
|
||||||
|
invoice=invoice2.invoice,
|
||||||
|
)
|
||||||
|
|
||||||
# The new credit note should be the residual of the cost of cart 1
|
# The new credit note should be the residual of the cost of cart 1
|
||||||
# minus the cost of cart 2.
|
# minus the cost of cart 2.
|
||||||
|
@ -385,7 +403,7 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
invoice.refund()
|
invoice.refund()
|
||||||
|
|
||||||
# There should be one credit note generated out of the invoice.
|
# There should be one credit note generated out of the invoice.
|
||||||
credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice)
|
credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice)
|
||||||
cn = TestingCreditNoteController(credit_note)
|
cn = TestingCreditNoteController(credit_note)
|
||||||
|
|
||||||
# Create a new cart with invoice, pay it
|
# Create a new cart with invoice, pay it
|
||||||
|
@ -426,15 +444,15 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
|
|
||||||
invoice.refund()
|
invoice.refund()
|
||||||
|
|
||||||
self.assertEquals(1, rego.CreditNote.unclaimed().count())
|
self.assertEquals(1, commerce.CreditNote.unclaimed().count())
|
||||||
|
|
||||||
credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice)
|
credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice)
|
||||||
|
|
||||||
cn = TestingCreditNoteController(credit_note)
|
cn = TestingCreditNoteController(credit_note)
|
||||||
cn.refund()
|
cn.refund()
|
||||||
|
|
||||||
# Refunding a credit note should mark it as claimed
|
# Refunding a credit note should mark it as claimed
|
||||||
self.assertEquals(0, rego.CreditNote.unclaimed().count())
|
self.assertEquals(0, commerce.CreditNote.unclaimed().count())
|
||||||
|
|
||||||
# Create a new cart with invoice
|
# Create a new cart with invoice
|
||||||
cart = TestingCartController.for_user(self.USER_1)
|
cart = TestingCartController.for_user(self.USER_1)
|
||||||
|
@ -458,9 +476,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
|
|
||||||
invoice.refund()
|
invoice.refund()
|
||||||
|
|
||||||
self.assertEquals(1, rego.CreditNote.unclaimed().count())
|
self.assertEquals(1, commerce.CreditNote.unclaimed().count())
|
||||||
|
|
||||||
credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice)
|
credit_note = commerce.CreditNote.objects.get(invoice=invoice.invoice)
|
||||||
|
|
||||||
cn = TestingCreditNoteController(credit_note)
|
cn = TestingCreditNoteController(credit_note)
|
||||||
|
|
||||||
|
@ -471,7 +489,7 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart))
|
invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart))
|
||||||
cn.apply_to_invoice(invoice_2.invoice)
|
cn.apply_to_invoice(invoice_2.invoice)
|
||||||
|
|
||||||
self.assertEquals(0, rego.CreditNote.unclaimed().count())
|
self.assertEquals(0, commerce.CreditNote.unclaimed().count())
|
||||||
|
|
||||||
# Cannot refund this credit note as it is already applied.
|
# Cannot refund this credit note as it is already applied.
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
|
|
|
@ -5,7 +5,8 @@ from decimal import Decimal
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion.models import conditions
|
||||||
|
from registrasion.models import inventory
|
||||||
from controller_helpers import TestingCartController
|
from controller_helpers import TestingCartController
|
||||||
from controller_helpers import TestingInvoiceController
|
from controller_helpers import TestingInvoiceController
|
||||||
|
|
||||||
|
@ -32,14 +33,14 @@ class VoucherTestCases(RegistrationCartTestCase):
|
||||||
|
|
||||||
# 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(inventory.Voucher.RESERVATION_DURATION * 2)
|
||||||
cart_2.apply_voucher(voucher.code)
|
cart_2.apply_voucher(voucher.code)
|
||||||
|
|
||||||
cart_2.next_cart()
|
cart_2.next_cart()
|
||||||
|
|
||||||
# After the reservation duration, even though the voucher has applied,
|
# After the reservation duration, even though the voucher has applied,
|
||||||
# it exceeds the number of vouchers available.
|
# it exceeds the number of vouchers available.
|
||||||
self.add_timedelta(rego.Voucher.RESERVATION_DURATION * 2)
|
self.add_timedelta(inventory.Voucher.RESERVATION_DURATION * 2)
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
cart_1.validate_cart()
|
cart_1.validate_cart()
|
||||||
|
|
||||||
|
@ -58,10 +59,10 @@ class VoucherTestCases(RegistrationCartTestCase):
|
||||||
def test_voucher_enables_item(self):
|
def test_voucher_enables_item(self):
|
||||||
voucher = self.new_voucher()
|
voucher = self.new_voucher()
|
||||||
|
|
||||||
flag = rego.VoucherFlag.objects.create(
|
flag = conditions.VoucherFlag.objects.create(
|
||||||
description="Voucher condition",
|
description="Voucher condition",
|
||||||
voucher=voucher,
|
voucher=voucher,
|
||||||
condition=rego.FlagBase.ENABLE_IF_TRUE,
|
condition=conditions.FlagBase.ENABLE_IF_TRUE,
|
||||||
)
|
)
|
||||||
flag.save()
|
flag.save()
|
||||||
flag.products.add(self.PROD_1)
|
flag.products.add(self.PROD_1)
|
||||||
|
@ -79,12 +80,12 @@ class VoucherTestCases(RegistrationCartTestCase):
|
||||||
def test_voucher_enables_discount(self):
|
def test_voucher_enables_discount(self):
|
||||||
voucher = self.new_voucher()
|
voucher = self.new_voucher()
|
||||||
|
|
||||||
discount = rego.VoucherDiscount.objects.create(
|
discount = conditions.VoucherDiscount.objects.create(
|
||||||
description="VOUCHER RECIPIENT",
|
description="VOUCHER RECIPIENT",
|
||||||
voucher=voucher,
|
voucher=voucher,
|
||||||
)
|
)
|
||||||
discount.save()
|
discount.save()
|
||||||
rego.DiscountForProduct.objects.create(
|
conditions.DiscountForProduct.objects.create(
|
||||||
discount=discount,
|
discount=discount,
|
||||||
product=self.PROD_1,
|
product=self.PROD_1,
|
||||||
percentage=Decimal(100),
|
percentage=Decimal(100),
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from registrasion import forms
|
from registrasion import forms
|
||||||
from registrasion import models as rego
|
from registrasion.models import commerce
|
||||||
|
from registrasion.models import inventory
|
||||||
|
from registrasion.models import people
|
||||||
from registrasion.controllers import discount
|
from registrasion.controllers import discount
|
||||||
from registrasion.controllers.cart import CartController
|
from registrasion.controllers.cart import CartController
|
||||||
from registrasion.controllers.credit_note import CreditNoteController
|
from registrasion.controllers.credit_note import CreditNoteController
|
||||||
|
@ -60,7 +62,7 @@ def guided_registration(request, page_id=0):
|
||||||
|
|
||||||
sections = []
|
sections = []
|
||||||
|
|
||||||
attendee = rego.Attendee.get_instance(request.user)
|
attendee = people.Attendee.get_instance(request.user)
|
||||||
|
|
||||||
if attendee.completed_registration:
|
if attendee.completed_registration:
|
||||||
return render(
|
return render(
|
||||||
|
@ -111,7 +113,7 @@ def guided_registration(request, page_id=0):
|
||||||
starting = attendee.guided_categories_complete.count() == 0
|
starting = attendee.guided_categories_complete.count() == 0
|
||||||
|
|
||||||
# Get the next category
|
# Get the next category
|
||||||
cats = rego.Category.objects
|
cats = inventory.Category.objects
|
||||||
if SESSION_KEY in request.session:
|
if SESSION_KEY in request.session:
|
||||||
_cats = request.session[SESSION_KEY]
|
_cats = request.session[SESSION_KEY]
|
||||||
cats = cats.filter(id__in=_cats)
|
cats = cats.filter(id__in=_cats)
|
||||||
|
@ -134,7 +136,7 @@ def guided_registration(request, page_id=0):
|
||||||
current_step = 3
|
current_step = 3
|
||||||
title = "Additional items"
|
title = "Additional items"
|
||||||
|
|
||||||
all_products = rego.Product.objects.filter(
|
all_products = inventory.Product.objects.filter(
|
||||||
category__in=cats,
|
category__in=cats,
|
||||||
).select_related("category")
|
).select_related("category")
|
||||||
|
|
||||||
|
@ -217,11 +219,13 @@ def edit_profile(request):
|
||||||
def handle_profile(request, prefix):
|
def handle_profile(request, prefix):
|
||||||
''' Returns a profile form instance, and a boolean which is true if the
|
''' Returns a profile form instance, and a boolean which is true if the
|
||||||
form was handled. '''
|
form was handled. '''
|
||||||
attendee = rego.Attendee.get_instance(request.user)
|
attendee = people.Attendee.get_instance(request.user)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profile = attendee.attendeeprofilebase
|
profile = attendee.attendeeprofilebase
|
||||||
profile = rego.AttendeeProfileBase.objects.get_subclass(pk=profile.id)
|
profile = people.AttendeeProfileBase.objects.get_subclass(
|
||||||
|
pk=profile.id,
|
||||||
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
profile = None
|
profile = None
|
||||||
|
|
||||||
|
@ -270,7 +274,7 @@ def product_category(request, category_id):
|
||||||
voucher_form, voucher_handled = v
|
voucher_form, voucher_handled = v
|
||||||
|
|
||||||
category_id = int(category_id) # Routing is [0-9]+
|
category_id = int(category_id) # Routing is [0-9]+
|
||||||
category = rego.Category.objects.get(pk=category_id)
|
category = inventory.Category.objects.get(pk=category_id)
|
||||||
|
|
||||||
products = ProductController.available_products(
|
products = ProductController.available_products(
|
||||||
request.user,
|
request.user,
|
||||||
|
@ -316,7 +320,7 @@ def handle_products(request, category, products, prefix):
|
||||||
ProductsForm = forms.ProductsForm(category, products)
|
ProductsForm = forms.ProductsForm(category, products)
|
||||||
|
|
||||||
# Create initial data for each of products in category
|
# Create initial data for each of products in category
|
||||||
items = rego.ProductItem.objects.filter(
|
items = commerce.ProductItem.objects.filter(
|
||||||
product__in=products,
|
product__in=products,
|
||||||
cart=current_cart.cart,
|
cart=current_cart.cart,
|
||||||
).select_related("product")
|
).select_related("product")
|
||||||
|
@ -344,8 +348,8 @@ def handle_products(request, category, products, prefix):
|
||||||
# If category is required, the user must have at least one
|
# If category is required, the user must have at least one
|
||||||
# in an active+valid cart
|
# in an active+valid cart
|
||||||
if category.required:
|
if category.required:
|
||||||
carts = rego.Cart.objects.filter(user=request.user)
|
carts = commerce.Cart.objects.filter(user=request.user)
|
||||||
items = rego.ProductItem.objects.filter(
|
items = commerce.ProductItem.objects.filter(
|
||||||
product__category=category,
|
product__category=category,
|
||||||
cart=carts,
|
cart=carts,
|
||||||
)
|
)
|
||||||
|
@ -366,7 +370,7 @@ def set_quantities_from_products_form(products_form, current_cart):
|
||||||
quantities = list(products_form.product_quantities())
|
quantities = list(products_form.product_quantities())
|
||||||
|
|
||||||
pks = [i[0] for i in quantities]
|
pks = [i[0] for i in quantities]
|
||||||
products = rego.Product.objects.filter(
|
products = inventory.Product.objects.filter(
|
||||||
id__in=pks,
|
id__in=pks,
|
||||||
).select_related("category")
|
).select_related("category")
|
||||||
|
|
||||||
|
@ -384,7 +388,7 @@ def set_quantities_from_products_form(products_form, current_cart):
|
||||||
product, message = ve_field.message
|
product, message = ve_field.message
|
||||||
if product in field_names:
|
if product in field_names:
|
||||||
field = field_names[product]
|
field = field_names[product]
|
||||||
elif isinstance(product, rego.Product):
|
elif isinstance(product, inventory.Product):
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
field = None
|
field = None
|
||||||
|
@ -402,7 +406,7 @@ def handle_voucher(request, prefix):
|
||||||
voucher_form.cleaned_data["voucher"].strip()):
|
voucher_form.cleaned_data["voucher"].strip()):
|
||||||
|
|
||||||
voucher = voucher_form.cleaned_data["voucher"]
|
voucher = voucher_form.cleaned_data["voucher"]
|
||||||
voucher = rego.Voucher.normalise_code(voucher)
|
voucher = inventory.Voucher.normalise_code(voucher)
|
||||||
|
|
||||||
if len(current_cart.cart.vouchers.filter(code=voucher)) > 0:
|
if len(current_cart.cart.vouchers.filter(code=voucher)) > 0:
|
||||||
# This voucher has already been applied to this cart.
|
# This voucher has already been applied to this cart.
|
||||||
|
@ -457,9 +461,9 @@ def invoice_access(request, access_code):
|
||||||
''' Redirects to the first unpaid invoice for the attendee that matches
|
''' Redirects to the first unpaid invoice for the attendee that matches
|
||||||
the given access code, if any. '''
|
the given access code, if any. '''
|
||||||
|
|
||||||
invoices = rego.Invoice.objects.filter(
|
invoices = commerce.Invoice.objects.filter(
|
||||||
user__attendee__access_code=access_code,
|
user__attendee__access_code=access_code,
|
||||||
status=rego.Invoice.STATUS_UNPAID,
|
status=commerce.Invoice.STATUS_UNPAID,
|
||||||
).order_by("issue_time")
|
).order_by("issue_time")
|
||||||
|
|
||||||
if not invoices:
|
if not invoices:
|
||||||
|
@ -478,7 +482,7 @@ def invoice(request, invoice_id, access_code=None):
|
||||||
'''
|
'''
|
||||||
|
|
||||||
invoice_id = int(invoice_id)
|
invoice_id = int(invoice_id)
|
||||||
inv = rego.Invoice.objects.get(pk=invoice_id)
|
inv = commerce.Invoice.objects.get(pk=invoice_id)
|
||||||
|
|
||||||
current_invoice = InvoiceController(inv)
|
current_invoice = InvoiceController(inv)
|
||||||
|
|
||||||
|
@ -505,7 +509,7 @@ def manual_payment(request, invoice_id):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
invoice_id = int(invoice_id)
|
invoice_id = int(invoice_id)
|
||||||
inv = get_object_or_404(rego.Invoice, pk=invoice_id)
|
inv = get_object_or_404(commerce.Invoice, pk=invoice_id)
|
||||||
current_invoice = InvoiceController(inv)
|
current_invoice = InvoiceController(inv)
|
||||||
|
|
||||||
form = forms.ManualPaymentForm(
|
form = forms.ManualPaymentForm(
|
||||||
|
@ -536,7 +540,7 @@ def refund(request, invoice_id):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
invoice_id = int(invoice_id)
|
invoice_id = int(invoice_id)
|
||||||
inv = get_object_or_404(rego.Invoice, pk=invoice_id)
|
inv = get_object_or_404(commerce.Invoice, pk=invoice_id)
|
||||||
current_invoice = InvoiceController(inv)
|
current_invoice = InvoiceController(inv)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -557,7 +561,7 @@ def credit_note(request, note_id, access_code=None):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
note_id = int(note_id)
|
note_id = int(note_id)
|
||||||
note = rego.CreditNote.objects.get(pk=note_id)
|
note = commerce.CreditNote.objects.get(pk=note_id)
|
||||||
|
|
||||||
current_note = CreditNoteController(note)
|
current_note = CreditNoteController(note)
|
||||||
|
|
||||||
|
@ -574,10 +578,11 @@ def credit_note(request, note_id, access_code=None):
|
||||||
|
|
||||||
if request.POST and apply_form.is_valid():
|
if request.POST and apply_form.is_valid():
|
||||||
inv_id = apply_form.cleaned_data["invoice"]
|
inv_id = apply_form.cleaned_data["invoice"]
|
||||||
invoice = rego.Invoice.objects.get(pk=inv_id)
|
invoice = commerce.Invoice.objects.get(pk=inv_id)
|
||||||
current_note.apply_to_invoice(invoice)
|
current_note.apply_to_invoice(invoice)
|
||||||
messages.success(request,
|
messages.success(
|
||||||
"Applied credit note %d to invoice." % note_id
|
request,
|
||||||
|
"Applied credit note %d to invoice." % note_id,
|
||||||
)
|
)
|
||||||
return redirect("invoice", invoice.id)
|
return redirect("invoice", invoice.id)
|
||||||
|
|
||||||
|
@ -585,7 +590,8 @@ def credit_note(request, note_id, access_code=None):
|
||||||
refund_form.instance.entered_by = request.user
|
refund_form.instance.entered_by = request.user
|
||||||
refund_form.instance.parent = note
|
refund_form.instance.parent = note
|
||||||
refund_form.save()
|
refund_form.save()
|
||||||
messages.success(request,
|
messages.success(
|
||||||
|
request,
|
||||||
"Applied manual refund to credit note."
|
"Applied manual refund to credit note."
|
||||||
)
|
)
|
||||||
return redirect("invoice", invoice.id)
|
return redirect("invoice", invoice.id)
|
||||||
|
|
Loading…
Reference in a new issue