Merge branch 'models-reorg'
This commit is contained in:
commit
539dbd3525
24 changed files with 1193 additions and 1040 deletions
|
@ -4,7 +4,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
import nested_admin
|
||||
|
||||
from registrasion import models as rego
|
||||
from registrasion.models import conditions
|
||||
from registrasion.models import inventory
|
||||
|
||||
|
||||
class EffectsDisplayMixin(object):
|
||||
|
@ -15,12 +16,12 @@ class EffectsDisplayMixin(object):
|
|||
|
||||
|
||||
class ProductInline(admin.TabularInline):
|
||||
model = rego.Product
|
||||
model = inventory.Product
|
||||
|
||||
|
||||
@admin.register(rego.Category)
|
||||
@admin.register(inventory.Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
model = rego.Category
|
||||
model = inventory.Category
|
||||
fields = ("name", "description", "required", "render_type",
|
||||
"limit_per_user", "order",)
|
||||
list_display = ("name", "description")
|
||||
|
@ -29,9 +30,9 @@ class CategoryAdmin(admin.ModelAdmin):
|
|||
]
|
||||
|
||||
|
||||
@admin.register(rego.Product)
|
||||
@admin.register(inventory.Product)
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
model = rego.Product
|
||||
model = inventory.Product
|
||||
list_display = ("name", "category", "description")
|
||||
list_filter = ("category", )
|
||||
|
||||
|
@ -39,18 +40,18 @@ class ProductAdmin(admin.ModelAdmin):
|
|||
# Discounts
|
||||
|
||||
class DiscountForProductInline(admin.TabularInline):
|
||||
model = rego.DiscountForProduct
|
||||
model = conditions.DiscountForProduct
|
||||
verbose_name = _("Product included in discount")
|
||||
verbose_name_plural = _("Products included in discount")
|
||||
|
||||
|
||||
class DiscountForCategoryInline(admin.TabularInline):
|
||||
model = rego.DiscountForCategory
|
||||
model = conditions.DiscountForCategory
|
||||
verbose_name = _("Category included in discount")
|
||||
verbose_name_plural = _("Categories included in discount")
|
||||
|
||||
|
||||
@admin.register(rego.TimeOrStockLimitDiscount)
|
||||
@admin.register(conditions.TimeOrStockLimitDiscount)
|
||||
class TimeOrStockLimitDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
|
||||
list_display = (
|
||||
"description",
|
||||
|
@ -67,7 +68,7 @@ class TimeOrStockLimitDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
|
|||
]
|
||||
|
||||
|
||||
@admin.register(rego.IncludedProductDiscount)
|
||||
@admin.register(conditions.IncludedProductDiscount)
|
||||
class IncludedProductDiscountAdmin(admin.ModelAdmin):
|
||||
|
||||
def enablers(self, obj):
|
||||
|
@ -87,7 +88,7 @@ class IncludedProductDiscountAdmin(admin.ModelAdmin):
|
|||
# Vouchers
|
||||
|
||||
class VoucherDiscountInline(nested_admin.NestedStackedInline):
|
||||
model = rego.VoucherDiscount
|
||||
model = conditions.VoucherDiscount
|
||||
verbose_name = _("Discount")
|
||||
|
||||
# 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):
|
||||
model = rego.VoucherFlag
|
||||
model = conditions.VoucherFlag
|
||||
verbose_name = _("Product and category enabled by voucher")
|
||||
verbose_name_plural = _("Products and categories enabled by voucher")
|
||||
|
||||
|
@ -109,7 +110,7 @@ class VoucherFlagInline(nested_admin.NestedStackedInline):
|
|||
extra = 1
|
||||
|
||||
|
||||
@admin.register(rego.Voucher)
|
||||
@admin.register(inventory.Voucher)
|
||||
class VoucherAdmin(nested_admin.NestedAdmin):
|
||||
|
||||
def effects(self, obj):
|
||||
|
@ -133,7 +134,7 @@ class VoucherAdmin(nested_admin.NestedAdmin):
|
|||
|
||||
return "\n".join(out)
|
||||
|
||||
model = rego.Voucher
|
||||
model = inventory.Voucher
|
||||
list_display = ("recipient", "code", "effects")
|
||||
inlines = [
|
||||
VoucherDiscountInline,
|
||||
|
@ -142,7 +143,7 @@ class VoucherAdmin(nested_admin.NestedAdmin):
|
|||
|
||||
|
||||
# Enabling conditions
|
||||
@admin.register(rego.ProductFlag)
|
||||
@admin.register(conditions.ProductFlag)
|
||||
class ProductFlagAdmin(
|
||||
nested_admin.NestedAdmin,
|
||||
EffectsDisplayMixin):
|
||||
|
@ -150,7 +151,7 @@ class ProductFlagAdmin(
|
|||
def enablers(self, obj):
|
||||
return list(obj.enabling_products.all())
|
||||
|
||||
model = rego.ProductFlag
|
||||
model = conditions.ProductFlag
|
||||
fields = ("description", "enabling_products", "condition", "products",
|
||||
"categories"),
|
||||
|
||||
|
@ -158,12 +159,12 @@ class ProductFlagAdmin(
|
|||
|
||||
|
||||
# Enabling conditions
|
||||
@admin.register(rego.CategoryFlag)
|
||||
@admin.register(conditions.CategoryFlag)
|
||||
class CategoryFlagAdmin(
|
||||
nested_admin.NestedAdmin,
|
||||
EffectsDisplayMixin):
|
||||
|
||||
model = rego.CategoryFlag
|
||||
model = conditions.CategoryFlag
|
||||
fields = ("description", "enabling_category", "condition", "products",
|
||||
"categories"),
|
||||
|
||||
|
@ -172,11 +173,11 @@ class CategoryFlagAdmin(
|
|||
|
||||
|
||||
# Enabling conditions
|
||||
@admin.register(rego.TimeOrStockLimitFlag)
|
||||
@admin.register(conditions.TimeOrStockLimitFlag)
|
||||
class TimeOrStockLimitFlagAdmin(
|
||||
nested_admin.NestedAdmin,
|
||||
EffectsDisplayMixin):
|
||||
model = rego.TimeOrStockLimitFlag
|
||||
model = conditions.TimeOrStockLimitFlag
|
||||
|
||||
list_display = (
|
||||
"description",
|
||||
|
|
|
@ -9,8 +9,10 @@ from django.db import transaction
|
|||
from django.db.models import Max
|
||||
from django.utils import timezone
|
||||
|
||||
from registrasion import models as rego
|
||||
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 conditions import ConditionController
|
||||
|
@ -28,9 +30,9 @@ class CartController(object):
|
|||
if there isn't one ready yet. '''
|
||||
|
||||
try:
|
||||
existing = rego.Cart.objects.get(user=user, active=True)
|
||||
existing = commerce.Cart.objects.get(user=user, active=True)
|
||||
except ObjectDoesNotExist:
|
||||
existing = rego.Cart.objects.create(
|
||||
existing = commerce.Cart.objects.create(
|
||||
user=user,
|
||||
time_last_updated=timezone.now(),
|
||||
reservation_duration=datetime.timedelta(),
|
||||
|
@ -47,10 +49,10 @@ class CartController(object):
|
|||
|
||||
# If we have vouchers, we're entitled to an hour at minimum.
|
||||
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
|
||||
items = rego.ProductItem.objects.filter(cart=self.cart)
|
||||
items = commerce.ProductItem.objects.filter(cart=self.cart)
|
||||
agg = items.aggregate(Max("product__reservation_duration"))
|
||||
product_max = agg["product__reservation_duration__max"]
|
||||
|
||||
|
@ -79,7 +81,7 @@ class CartController(object):
|
|||
is violated. `product_quantities` is an iterable of (product, quantity)
|
||||
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(
|
||||
"product",
|
||||
"product__category",
|
||||
|
@ -99,14 +101,14 @@ class CartController(object):
|
|||
|
||||
for product, quantity in product_quantities:
|
||||
try:
|
||||
product_item = rego.ProductItem.objects.get(
|
||||
product_item = commerce.ProductItem.objects.get(
|
||||
cart=self.cart,
|
||||
product=product,
|
||||
)
|
||||
product_item.quantity = quantity
|
||||
product_item.save()
|
||||
except ObjectDoesNotExist:
|
||||
rego.ProductItem.objects.create(
|
||||
commerce.ProductItem.objects.create(
|
||||
cart=self.cart,
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
|
@ -176,7 +178,7 @@ class CartController(object):
|
|||
''' Applies the voucher with the given code to this cart. '''
|
||||
|
||||
# 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
|
||||
if voucher in self.cart.vouchers.all():
|
||||
|
@ -193,7 +195,7 @@ class CartController(object):
|
|||
Raises ValidationError if not. '''
|
||||
|
||||
# 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
|
||||
carts_with_voucher = active_carts.filter(vouchers=voucher)
|
||||
|
@ -238,7 +240,7 @@ class CartController(object):
|
|||
except ValidationError as 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)
|
||||
try:
|
||||
|
@ -248,7 +250,7 @@ class CartController(object):
|
|||
errors.append(error.message[1])
|
||||
|
||||
# Validate the discounts
|
||||
discount_items = rego.DiscountItem.objects.filter(cart=cart)
|
||||
discount_items = commerce.DiscountItem.objects.filter(cart=cart)
|
||||
seen_discounts = set()
|
||||
|
||||
for discount_item in discount_items:
|
||||
|
@ -256,7 +258,7 @@ class CartController(object):
|
|||
if discount in seen_discounts:
|
||||
continue
|
||||
seen_discounts.add(discount)
|
||||
real_discount = rego.DiscountBase.objects.get_subclass(
|
||||
real_discount = conditions.DiscountBase.objects.get_subclass(
|
||||
pk=discount.pk)
|
||||
cond = ConditionController.for_condition(real_discount)
|
||||
|
||||
|
@ -287,7 +289,7 @@ class CartController(object):
|
|||
self.cart.vouchers.remove(voucher)
|
||||
|
||||
# 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")
|
||||
products = set(i.product for i in items)
|
||||
available = set(ProductController.available_products(
|
||||
|
@ -306,7 +308,7 @@ class CartController(object):
|
|||
'''
|
||||
|
||||
# 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", "product__category",
|
||||
|
@ -331,7 +333,7 @@ class CartController(object):
|
|||
def matches(discount):
|
||||
''' Returns True if and only if the given discount apples to
|
||||
our product. '''
|
||||
if isinstance(discount.clause, rego.DiscountForCategory):
|
||||
if isinstance(discount.clause, conditions.DiscountForCategory):
|
||||
return discount.clause.category == product.category
|
||||
else:
|
||||
return discount.clause.product == product
|
||||
|
@ -356,7 +358,7 @@ class CartController(object):
|
|||
|
||||
# Get a provisional instance for this DiscountItem
|
||||
# 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,
|
||||
cart=self.cart,
|
||||
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
|
||||
|
||||
|
@ -22,7 +23,9 @@ class CategoryController(object):
|
|||
from product import ProductController
|
||||
|
||||
if products is AllProducts:
|
||||
products = rego.Product.objects.all().select_related("category")
|
||||
products = inventory.Product.objects.all().select_related(
|
||||
"category",
|
||||
)
|
||||
|
||||
available = ProductController.available_products(
|
||||
user,
|
||||
|
@ -41,13 +44,13 @@ class CategoryController(object):
|
|||
# We don't need to waste the following queries
|
||||
return 99999999
|
||||
|
||||
carts = rego.Cart.objects.filter(
|
||||
carts = commerce.Cart.objects.filter(
|
||||
user=user,
|
||||
active=False,
|
||||
released=False,
|
||||
)
|
||||
|
||||
items = rego.ProductItem.objects.filter(
|
||||
items = commerce.ProductItem.objects.filter(
|
||||
cart__in=carts,
|
||||
product__category=self.category,
|
||||
)
|
||||
|
|
|
@ -7,7 +7,9 @@ from collections import namedtuple
|
|||
from django.db.models import Sum
|
||||
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(
|
||||
|
@ -29,15 +31,15 @@ class ConditionController(object):
|
|||
@staticmethod
|
||||
def for_condition(condition):
|
||||
CONTROLLERS = {
|
||||
rego.CategoryFlag: CategoryConditionController,
|
||||
rego.IncludedProductDiscount: ProductConditionController,
|
||||
rego.ProductFlag: ProductConditionController,
|
||||
rego.TimeOrStockLimitDiscount:
|
||||
conditions.CategoryFlag: CategoryConditionController,
|
||||
conditions.IncludedProductDiscount: ProductConditionController,
|
||||
conditions.ProductFlag: ProductConditionController,
|
||||
conditions.TimeOrStockLimitDiscount:
|
||||
TimeOrStockLimitDiscountController,
|
||||
rego.TimeOrStockLimitFlag:
|
||||
conditions.TimeOrStockLimitFlag:
|
||||
TimeOrStockLimitFlagController,
|
||||
rego.VoucherDiscount: VoucherConditionController,
|
||||
rego.VoucherFlag: VoucherConditionController,
|
||||
conditions.VoucherDiscount: VoucherConditionController,
|
||||
conditions.VoucherFlag: VoucherConditionController,
|
||||
}
|
||||
|
||||
try:
|
||||
|
@ -121,7 +123,7 @@ class ConditionController(object):
|
|||
# Get all products covered by this condition, and the products
|
||||
# from the categories covered by this condition
|
||||
cond_products = condition.products.all()
|
||||
from_category = rego.Product.objects.filter(
|
||||
from_category = inventory.Product.objects.filter(
|
||||
category__in=condition.categories.all(),
|
||||
).all()
|
||||
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
|
||||
this condition in one of their carts '''
|
||||
|
||||
carts = rego.Cart.objects.filter(user=user, released=False)
|
||||
enabling_products = rego.Product.objects.filter(
|
||||
carts = commerce.Cart.objects.filter(user=user, released=False)
|
||||
enabling_products = inventory.Product.objects.filter(
|
||||
category=self.condition.enabling_category,
|
||||
)
|
||||
products_count = rego.ProductItem.objects.filter(
|
||||
products_count = commerce.ProductItem.objects.filter(
|
||||
cart__in=carts,
|
||||
product__in=enabling_products,
|
||||
).count()
|
||||
|
@ -221,8 +223,8 @@ class ProductConditionController(ConditionController):
|
|||
''' returns True if the user has a product that invokes this
|
||||
condition in one of their carts '''
|
||||
|
||||
carts = rego.Cart.objects.filter(user=user, released=False)
|
||||
products_count = rego.ProductItem.objects.filter(
|
||||
carts = commerce.Cart.objects.filter(user=user, released=False)
|
||||
products_count = commerce.ProductItem.objects.filter(
|
||||
cart__in=carts,
|
||||
product__in=self.condition.enabling_products.all(),
|
||||
).count()
|
||||
|
@ -267,7 +269,7 @@ class TimeOrStockLimitConditionController(ConditionController):
|
|||
return 99999999
|
||||
|
||||
# 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(
|
||||
user=user,
|
||||
active=True,
|
||||
|
@ -284,12 +286,12 @@ class TimeOrStockLimitFlagController(
|
|||
TimeOrStockLimitConditionController):
|
||||
|
||||
def _items(self):
|
||||
category_products = rego.Product.objects.filter(
|
||||
category_products = inventory.Product.objects.filter(
|
||||
category__in=self.ceiling.categories.all(),
|
||||
)
|
||||
products = self.ceiling.products.all() | category_products
|
||||
|
||||
product_items = rego.ProductItem.objects.filter(
|
||||
product_items = commerce.ProductItem.objects.filter(
|
||||
product__in=products.all(),
|
||||
)
|
||||
return product_items
|
||||
|
@ -298,7 +300,7 @@ class TimeOrStockLimitFlagController(
|
|||
class TimeOrStockLimitDiscountController(TimeOrStockLimitConditionController):
|
||||
|
||||
def _items(self):
|
||||
discount_items = rego.DiscountItem.objects.filter(
|
||||
discount_items = commerce.DiscountItem.objects.filter(
|
||||
discount=self.ceiling,
|
||||
)
|
||||
return discount_items
|
||||
|
@ -312,7 +314,7 @@ class VoucherConditionController(ConditionController):
|
|||
|
||||
def is_met(self, user):
|
||||
''' returns True if the user has the given voucher attached. '''
|
||||
carts_count = rego.Cart.objects.filter(
|
||||
carts_count = commerce.Cart.objects.filter(
|
||||
user=user,
|
||||
vouchers=self.condition.voucher,
|
||||
).count()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.db import transaction
|
||||
|
||||
from registrasion import models as rego
|
||||
from registrasion.models import commerce
|
||||
|
||||
|
||||
class CreditNoteController(object):
|
||||
|
@ -14,7 +14,7 @@ class CreditNoteController(object):
|
|||
the given invoice. You need to call InvoiceController.update_status()
|
||||
to set the status correctly, if appropriate. '''
|
||||
|
||||
credit_note = rego.CreditNote.objects.create(
|
||||
credit_note = commerce.CreditNote.objects.create(
|
||||
invoice=invoice,
|
||||
amount=0-value, # Credit notes start off as a payment against inv.
|
||||
reference="ONE MOMENT",
|
||||
|
@ -39,7 +39,7 @@ class CreditNoteController(object):
|
|||
inv.validate_allowed_to_pay()
|
||||
|
||||
# Apply payment to invoice
|
||||
rego.CreditNoteApplication.objects.create(
|
||||
commerce.CreditNoteApplication.objects.create(
|
||||
parent=self.credit_note,
|
||||
invoice=invoice,
|
||||
amount=self.credit_note.value,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import itertools
|
||||
|
||||
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
|
||||
|
||||
|
@ -24,15 +25,15 @@ def available_discounts(user, categories, products):
|
|||
not including products that are pending purchase. '''
|
||||
|
||||
# discounts that match provided categories
|
||||
category_discounts = rego.DiscountForCategory.objects.filter(
|
||||
category_discounts = conditions.DiscountForCategory.objects.filter(
|
||||
category__in=categories
|
||||
)
|
||||
# discounts that match provided products
|
||||
product_discounts = rego.DiscountForProduct.objects.filter(
|
||||
product_discounts = conditions.DiscountForProduct.objects.filter(
|
||||
product__in=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)
|
||||
)
|
||||
# (Not relevant: discounts that match products in provided categories)
|
||||
|
@ -60,7 +61,7 @@ def available_discounts(user, categories, products):
|
|||
failed_discounts = set()
|
||||
|
||||
for discount in potential_discounts:
|
||||
real_discount = rego.DiscountBase.objects.get_subclass(
|
||||
real_discount = conditions.DiscountBase.objects.get_subclass(
|
||||
pk=discount.discount.pk,
|
||||
)
|
||||
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.
|
||||
# If this user has exceeded the limit for the clause, this clause
|
||||
# is not available any more.
|
||||
past_uses = rego.DiscountItem.objects.filter(
|
||||
past_uses = commerce.DiscountItem.objects.filter(
|
||||
cart__user=user,
|
||||
cart__active=False, # Only past carts count
|
||||
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.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 credit_note import CreditNoteController
|
||||
|
@ -25,8 +27,8 @@ class InvoiceController(object):
|
|||
an invoice is generated.'''
|
||||
|
||||
try:
|
||||
invoice = rego.Invoice.objects.exclude(
|
||||
status=rego.Invoice.STATUS_VOID,
|
||||
invoice = commerce.Invoice.objects.exclude(
|
||||
status=commerce.Invoice.STATUS_VOID,
|
||||
).get(
|
||||
cart=cart,
|
||||
cart_revision=cart.revision,
|
||||
|
@ -42,19 +44,19 @@ class InvoiceController(object):
|
|||
|
||||
@classmethod
|
||||
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:
|
||||
cls(invoice).void()
|
||||
|
||||
@classmethod
|
||||
def resolve_discount_value(cls, item):
|
||||
try:
|
||||
condition = rego.DiscountForProduct.objects.get(
|
||||
condition = conditions.DiscountForProduct.objects.get(
|
||||
discount=item.discount,
|
||||
product=item.product
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
condition = rego.DiscountForCategory.objects.get(
|
||||
condition = conditions.DiscountForCategory.objects.get(
|
||||
discount=item.discount,
|
||||
category=item.product.category
|
||||
)
|
||||
|
@ -75,22 +77,22 @@ class InvoiceController(object):
|
|||
due = max(issued, reservation_limit)
|
||||
|
||||
# Get the invoice recipient
|
||||
profile = rego.AttendeeProfileBase.objects.get_subclass(
|
||||
profile = people.AttendeeProfileBase.objects.get_subclass(
|
||||
id=cart.user.attendee.attendeeprofilebase.id,
|
||||
)
|
||||
recipient = profile.invoice_recipient()
|
||||
invoice = rego.Invoice.objects.create(
|
||||
invoice = commerce.Invoice.objects.create(
|
||||
user=cart.user,
|
||||
cart=cart,
|
||||
cart_revision=cart.revision,
|
||||
status=rego.Invoice.STATUS_UNPAID,
|
||||
status=commerce.Invoice.STATUS_UNPAID,
|
||||
value=Decimal(),
|
||||
issue_time=issued,
|
||||
due_time=due,
|
||||
recipient=recipient,
|
||||
)
|
||||
|
||||
product_items = rego.ProductItem.objects.filter(cart=cart)
|
||||
product_items = commerce.ProductItem.objects.filter(cart=cart)
|
||||
|
||||
if len(product_items) == 0:
|
||||
raise ValidationError("Your cart is empty.")
|
||||
|
@ -98,11 +100,11 @@ class InvoiceController(object):
|
|||
product_items = product_items.order_by(
|
||||
"product__category__order", "product__order"
|
||||
)
|
||||
discount_items = rego.DiscountItem.objects.filter(cart=cart)
|
||||
discount_items = commerce.DiscountItem.objects.filter(cart=cart)
|
||||
invoice_value = Decimal()
|
||||
for item in product_items:
|
||||
product = item.product
|
||||
line_item = rego.LineItem.objects.create(
|
||||
line_item = commerce.LineItem.objects.create(
|
||||
invoice=invoice,
|
||||
description="%s - %s" % (product.category.name, product.name),
|
||||
quantity=item.quantity,
|
||||
|
@ -112,7 +114,7 @@ class InvoiceController(object):
|
|||
invoice_value += line_item.quantity * line_item.price
|
||||
|
||||
for item in discount_items:
|
||||
line_item = rego.LineItem.objects.create(
|
||||
line_item = commerce.LineItem.objects.create(
|
||||
invoice=invoice,
|
||||
description=item.discount.description,
|
||||
quantity=item.quantity,
|
||||
|
@ -170,7 +172,7 @@ class InvoiceController(object):
|
|||
def total_payments(self):
|
||||
''' 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
|
||||
return total_paid
|
||||
|
||||
|
@ -180,12 +182,12 @@ class InvoiceController(object):
|
|||
|
||||
old_status = self.invoice.status
|
||||
total_paid = self.total_payments()
|
||||
num_payments = rego.PaymentBase.objects.filter(
|
||||
num_payments = commerce.PaymentBase.objects.filter(
|
||||
invoice=self.invoice,
|
||||
).count()
|
||||
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
|
||||
if remainder <= 0:
|
||||
# Invoice no longer has amount owing
|
||||
|
@ -199,15 +201,15 @@ class InvoiceController(object):
|
|||
elif total_paid == 0 and num_payments > 0:
|
||||
# Invoice has multiple payments totalling zero
|
||||
self._mark_void()
|
||||
elif old_status == rego.Invoice.STATUS_PAID:
|
||||
elif old_status == commerce.Invoice.STATUS_PAID:
|
||||
if remainder > 0:
|
||||
# Invoice went from having a remainder of zero or less
|
||||
# to having a positive remainder -- must be a refund
|
||||
self._mark_refunded()
|
||||
elif old_status == rego.Invoice.STATUS_REFUNDED:
|
||||
elif old_status == commerce.Invoice.STATUS_REFUNDED:
|
||||
# Should not ever change from here
|
||||
pass
|
||||
elif old_status == rego.Invoice.STATUS_VOID:
|
||||
elif old_status == commerce.Invoice.STATUS_VOID:
|
||||
# Should not ever change from here
|
||||
pass
|
||||
|
||||
|
@ -218,7 +220,7 @@ class InvoiceController(object):
|
|||
if cart:
|
||||
cart.active = False
|
||||
cart.save()
|
||||
self.invoice.status = rego.Invoice.STATUS_PAID
|
||||
self.invoice.status = commerce.Invoice.STATUS_PAID
|
||||
self.invoice.save()
|
||||
|
||||
def _mark_refunded(self):
|
||||
|
@ -229,13 +231,13 @@ class InvoiceController(object):
|
|||
cart.active = False
|
||||
cart.released = True
|
||||
cart.save()
|
||||
self.invoice.status = rego.Invoice.STATUS_REFUNDED
|
||||
self.invoice.status = commerce.Invoice.STATUS_REFUNDED
|
||||
self.invoice.save()
|
||||
|
||||
def _mark_void(self):
|
||||
''' Marks the invoice as refunded, and updates the attached cart if
|
||||
necessary. '''
|
||||
self.invoice.status = rego.Invoice.STATUS_VOID
|
||||
self.invoice.status = commerce.Invoice.STATUS_VOID
|
||||
self.invoice.save()
|
||||
|
||||
def _invoice_matches_cart(self):
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import itertools
|
||||
|
||||
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 conditions import ConditionController
|
||||
|
@ -22,7 +23,7 @@ class ProductController(object):
|
|||
raise ValueError("You must provide products or a category")
|
||||
|
||||
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")
|
||||
else:
|
||||
all_products = []
|
||||
|
@ -65,13 +66,13 @@ class ProductController(object):
|
|||
# Don't need to run the remaining queries
|
||||
return 999999 # We can do better
|
||||
|
||||
carts = rego.Cart.objects.filter(
|
||||
carts = commerce.Cart.objects.filter(
|
||||
user=user,
|
||||
active=False,
|
||||
released=False,
|
||||
)
|
||||
|
||||
items = rego.ProductItem.objects.filter(
|
||||
items = commerce.ProductItem.objects.filter(
|
||||
cart__in=carts,
|
||||
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
|
||||
|
||||
|
@ -14,8 +15,8 @@ class ApplyCreditNoteForm(forms.Form):
|
|||
self.fields["invoice"].choices = self._unpaid_invoices_for_user
|
||||
|
||||
def _unpaid_invoices_for_user(self):
|
||||
invoices = rego.Invoice.objects.filter(
|
||||
status=rego.Invoice.STATUS_UNPAID,
|
||||
invoices = commerce.Invoice.objects.filter(
|
||||
status=commerce.Invoice.STATUS_UNPAID,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
|
@ -25,7 +26,6 @@ class ApplyCreditNoteForm(forms.Form):
|
|||
]
|
||||
|
||||
invoice = forms.ChoiceField(
|
||||
#choices=_unpaid_invoices_for_user,
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
@ -33,14 +33,14 @@ class ApplyCreditNoteForm(forms.Form):
|
|||
class ManualCreditNoteRefundForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = rego.ManualCreditNoteRefund
|
||||
model = commerce.ManualCreditNoteRefund
|
||||
fields = ["reference"]
|
||||
|
||||
|
||||
class ManualPaymentForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = rego.ManualPayment
|
||||
model = commerce.ManualPayment
|
||||
fields = ["reference", "amount"]
|
||||
|
||||
|
||||
|
@ -168,8 +168,8 @@ def ProductsForm(category, products):
|
|||
|
||||
# Each Category.RENDER_TYPE value has a subclass here.
|
||||
RENDER_TYPES = {
|
||||
rego.Category.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm,
|
||||
rego.Category.RENDER_TYPE_RADIO: _RadioButtonProductsForm,
|
||||
inventory.Category.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm,
|
||||
inventory.Category.RENDER_TYPE_RADIO: _RadioButtonProductsForm,
|
||||
}
|
||||
|
||||
# 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 collections import namedtuple
|
||||
|
@ -19,7 +20,7 @@ def available_categories(context):
|
|||
@register.assignment_tag(takes_context=True)
|
||||
def available_credit(context):
|
||||
''' 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,
|
||||
)
|
||||
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)
|
||||
def invoices(context):
|
||||
''' 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)
|
||||
|
@ -37,7 +38,7 @@ def items_pending(context):
|
|||
''' Returns all of the items that this user has in their current cart,
|
||||
and is awaiting payment. '''
|
||||
|
||||
all_items = rego.ProductItem.objects.filter(
|
||||
all_items = commerce.ProductItem.objects.filter(
|
||||
cart__user=context.request.user,
|
||||
cart__active=True,
|
||||
).select_related(
|
||||
|
@ -55,7 +56,7 @@ def items_purchased(context, category=None):
|
|||
''' Returns all of the items that this user has purchased, optionally
|
||||
from the given category. '''
|
||||
|
||||
all_items = rego.ProductItem.objects.filter(
|
||||
all_items = commerce.ProductItem.objects.filter(
|
||||
cart__user=context.request.user,
|
||||
cart__active=False,
|
||||
cart__released=False,
|
||||
|
@ -65,7 +66,7 @@ def items_purchased(context, category=None):
|
|||
all_items = all_items.filter(product__category=category)
|
||||
|
||||
pq = all_items.values("product").annotate(quantity=Sum("quantity")).all()
|
||||
products = rego.Product.objects.all()
|
||||
products = inventory.Product.objects.all()
|
||||
out = []
|
||||
for item in pq:
|
||||
prod = products.get(pk=item["product"])
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from registrasion.controllers.cart import CartController
|
||||
from registrasion.controllers.credit_note import CreditNoteController
|
||||
from registrasion.controllers.invoice import InvoiceController
|
||||
from registrasion import models as rego
|
||||
from registrasion.models import commerce
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
|
@ -19,7 +19,7 @@ class TestingCartController(CartController):
|
|||
ValidationError if constraints are violated.'''
|
||||
|
||||
try:
|
||||
product_item = rego.ProductItem.objects.get(
|
||||
product_item = commerce.ProductItem.objects.get(
|
||||
cart=self.cart,
|
||||
product=product)
|
||||
old_quantity = product_item.quantity
|
||||
|
@ -41,7 +41,7 @@ class TestingInvoiceController(InvoiceController):
|
|||
self.validate_allowed_to_pay()
|
||||
|
||||
''' Adds a payment '''
|
||||
rego.ManualPayment.objects.create(
|
||||
commerce.ManualPayment.objects.create(
|
||||
invoice=self.invoice,
|
||||
reference=reference,
|
||||
amount=amount,
|
||||
|
@ -53,7 +53,7 @@ class TestingInvoiceController(InvoiceController):
|
|||
class TestingCreditNoteController(CreditNoteController):
|
||||
|
||||
def refund(self):
|
||||
rego.CreditNoteRefund.objects.create(
|
||||
commerce.CreditNoteRefund.objects.create(
|
||||
parent=self.credit_note,
|
||||
reference="Whoops."
|
||||
)
|
||||
|
|
|
@ -7,7 +7,10 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||
from django.core.exceptions import ValidationError
|
||||
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 controller_helpers import TestingCartController
|
||||
|
@ -36,24 +39,28 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
|||
email='test2@example.com',
|
||||
password='top_secret')
|
||||
|
||||
attendee1 = rego.Attendee.get_instance(cls.USER_1)
|
||||
attendee1 = people.Attendee.get_instance(cls.USER_1)
|
||||
attendee1.save()
|
||||
profile1 = rego.AttendeeProfileBase.objects.create(attendee=attendee1)
|
||||
profile1 = people.AttendeeProfileBase.objects.create(
|
||||
attendee=attendee1,
|
||||
)
|
||||
profile1.save()
|
||||
attendee2 = rego.Attendee.get_instance(cls.USER_2)
|
||||
attendee2 = people.Attendee.get_instance(cls.USER_2)
|
||||
attendee2.save()
|
||||
profile2 = rego.AttendeeProfileBase.objects.create(attendee=attendee2)
|
||||
profile2 = people.AttendeeProfileBase.objects.create(
|
||||
attendee=attendee2,
|
||||
)
|
||||
profile2.save()
|
||||
|
||||
cls.RESERVATION = datetime.timedelta(hours=1)
|
||||
|
||||
cls.categories = []
|
||||
for i in xrange(2):
|
||||
cat = rego.Category.objects.create(
|
||||
cat = inventory.Category.objects.create(
|
||||
name="Category " + str(i + 1),
|
||||
description="This is a test category",
|
||||
order=i,
|
||||
render_type=rego.Category.RENDER_TYPE_RADIO,
|
||||
render_type=inventory.Category.RENDER_TYPE_RADIO,
|
||||
required=False,
|
||||
)
|
||||
cat.save()
|
||||
|
@ -64,7 +71,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
|||
|
||||
cls.products = []
|
||||
for i in xrange(4):
|
||||
prod = rego.Product.objects.create(
|
||||
prod = inventory.Product.objects.create(
|
||||
name="Product " + str(i + 1),
|
||||
description="This is a test product.",
|
||||
category=cls.categories[i / 2], # 2 products per category
|
||||
|
@ -95,9 +102,9 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
|||
|
||||
@classmethod
|
||||
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,
|
||||
condition=rego.FlagBase.DISABLE_IF_FALSE,
|
||||
condition=conditions.FlagBase.DISABLE_IF_FALSE,
|
||||
limit=limit,
|
||||
start_time=start_time,
|
||||
end_time=end_time
|
||||
|
@ -109,9 +116,9 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
|||
@classmethod
|
||||
def make_category_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,
|
||||
condition=rego.FlagBase.DISABLE_IF_FALSE,
|
||||
condition=conditions.FlagBase.DISABLE_IF_FALSE,
|
||||
limit=limit,
|
||||
start_time=start_time,
|
||||
end_time=end_time
|
||||
|
@ -124,14 +131,14 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
|||
def make_discount_ceiling(
|
||||
cls, name, limit=None, start_time=None, end_time=None,
|
||||
percentage=100):
|
||||
limit_ceiling = rego.TimeOrStockLimitDiscount.objects.create(
|
||||
limit_ceiling = conditions.TimeOrStockLimitDiscount.objects.create(
|
||||
description=name,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
)
|
||||
limit_ceiling.save()
|
||||
rego.DiscountForProduct.objects.create(
|
||||
conditions.DiscountForProduct.objects.create(
|
||||
discount=limit_ceiling,
|
||||
product=cls.PROD_1,
|
||||
percentage=percentage,
|
||||
|
@ -140,7 +147,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
|||
|
||||
@classmethod
|
||||
def new_voucher(self, code="VOUCHER", limit=1):
|
||||
voucher = rego.Voucher.objects.create(
|
||||
voucher = inventory.Voucher.objects.create(
|
||||
recipient="Voucher recipient",
|
||||
code=code,
|
||||
limit=limit,
|
||||
|
@ -176,7 +183,7 @@ class BasicCartTests(RegistrationCartTestCase):
|
|||
current_cart.add_to_cart(self.PROD_1, 1)
|
||||
|
||||
# Count of products for a given user should be collapsed.
|
||||
items = rego.ProductItem.objects.filter(
|
||||
items = commerce.ProductItem.objects.filter(
|
||||
cart=current_cart.cart,
|
||||
product=self.PROD_1)
|
||||
self.assertEqual(1, len(items))
|
||||
|
@ -187,7 +194,7 @@ class BasicCartTests(RegistrationCartTestCase):
|
|||
current_cart = TestingCartController.for_user(self.USER_1)
|
||||
|
||||
def get_item():
|
||||
return rego.ProductItem.objects.get(
|
||||
return commerce.ProductItem.objects.get(
|
||||
cart=current_cart.cart,
|
||||
product=self.PROD_1)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError
|
|||
from controller_helpers import TestingCartController
|
||||
from test_cart import RegistrationCartTestCase
|
||||
|
||||
from registrasion import models as rego
|
||||
from registrasion.models import conditions
|
||||
|
||||
UTC = pytz.timezone('UTC')
|
||||
|
||||
|
@ -155,12 +155,12 @@ class CeilingsTestCases(RegistrationCartTestCase):
|
|||
self.make_discount_ceiling("Limit ceiling", limit=1, percentage=50)
|
||||
voucher = self.new_voucher(code="VOUCHER")
|
||||
|
||||
discount = rego.VoucherDiscount.objects.create(
|
||||
discount = conditions.VoucherDiscount.objects.create(
|
||||
description="VOUCHER RECIPIENT",
|
||||
voucher=voucher,
|
||||
)
|
||||
discount.save()
|
||||
rego.DiscountForProduct.objects.create(
|
||||
conditions.DiscountForProduct.objects.create(
|
||||
discount=discount,
|
||||
product=self.PROD_1,
|
||||
percentage=100,
|
||||
|
|
|
@ -2,7 +2,7 @@ import pytz
|
|||
|
||||
from decimal import Decimal
|
||||
|
||||
from registrasion import models as rego
|
||||
from registrasion.models import conditions
|
||||
from registrasion.controllers import discount
|
||||
from controller_helpers import TestingCartController
|
||||
|
||||
|
@ -19,13 +19,13 @@ class DiscountTestCase(RegistrationCartTestCase):
|
|||
amount=Decimal(100),
|
||||
quantity=2,
|
||||
):
|
||||
discount = rego.IncludedProductDiscount.objects.create(
|
||||
discount = conditions.IncludedProductDiscount.objects.create(
|
||||
description="PROD_1 includes PROD_2 " + str(amount) + "%",
|
||||
)
|
||||
discount.save()
|
||||
discount.enabling_products.add(cls.PROD_1)
|
||||
discount.save()
|
||||
rego.DiscountForProduct.objects.create(
|
||||
conditions.DiscountForProduct.objects.create(
|
||||
discount=discount,
|
||||
product=cls.PROD_2,
|
||||
percentage=amount,
|
||||
|
@ -39,13 +39,13 @@ class DiscountTestCase(RegistrationCartTestCase):
|
|||
amount=Decimal(100),
|
||||
quantity=2,
|
||||
):
|
||||
discount = rego.IncludedProductDiscount.objects.create(
|
||||
discount = conditions.IncludedProductDiscount.objects.create(
|
||||
description="PROD_1 includes CAT_2 " + str(amount) + "%",
|
||||
)
|
||||
discount.save()
|
||||
discount.enabling_products.add(cls.PROD_1)
|
||||
discount.save()
|
||||
rego.DiscountForCategory.objects.create(
|
||||
conditions.DiscountForCategory.objects.create(
|
||||
discount=discount,
|
||||
category=cls.CAT_2,
|
||||
percentage=amount,
|
||||
|
@ -59,20 +59,20 @@ class DiscountTestCase(RegistrationCartTestCase):
|
|||
amount=Decimal(100),
|
||||
quantity=2,
|
||||
):
|
||||
discount = rego.IncludedProductDiscount.objects.create(
|
||||
discount = conditions.IncludedProductDiscount.objects.create(
|
||||
description="PROD_1 includes PROD_3 and PROD_4 " +
|
||||
str(amount) + "%",
|
||||
)
|
||||
discount.save()
|
||||
discount.enabling_products.add(cls.PROD_1)
|
||||
discount.save()
|
||||
rego.DiscountForProduct.objects.create(
|
||||
conditions.DiscountForProduct.objects.create(
|
||||
discount=discount,
|
||||
product=cls.PROD_3,
|
||||
percentage=amount,
|
||||
quantity=quantity,
|
||||
).save()
|
||||
rego.DiscountForProduct.objects.create(
|
||||
conditions.DiscountForProduct.objects.create(
|
||||
discount=discount,
|
||||
product=cls.PROD_4,
|
||||
percentage=amount,
|
||||
|
|
|
@ -2,7 +2,8 @@ import pytz
|
|||
|
||||
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 controller_helpers import TestingCartController
|
||||
from registrasion.controllers.product import ProductController
|
||||
|
@ -15,10 +16,10 @@ UTC = pytz.timezone('UTC')
|
|||
class FlagTestCases(RegistrationCartTestCase):
|
||||
|
||||
@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
|
||||
predicated on adding PROD_2 beforehand. '''
|
||||
flag = rego.ProductFlag.objects.create(
|
||||
flag = conditions.ProductFlag.objects.create(
|
||||
description="Product condition",
|
||||
condition=condition,
|
||||
)
|
||||
|
@ -28,10 +29,13 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
flag.save()
|
||||
|
||||
@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:
|
||||
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",
|
||||
condition=condition,
|
||||
)
|
||||
|
@ -40,10 +44,10 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
flag.enabling_products.add(cls.PROD_3)
|
||||
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
|
||||
predicated on adding an item from CAT_2 beforehand.'''
|
||||
flag = rego.CategoryFlag.objects.create(
|
||||
flag = conditions.CategoryFlag.objects.create(
|
||||
description="Category condition",
|
||||
condition=condition,
|
||||
enabling_category=cls.CAT_2,
|
||||
|
@ -131,8 +135,8 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
cart_2.add_to_cart(self.PROD_1, 1)
|
||||
|
||||
def test_multiple_dif_conditions(self):
|
||||
self.add_product_flag(condition=rego.FlagBase.DISABLE_IF_FALSE)
|
||||
self.add_category_flag(condition=rego.FlagBase.DISABLE_IF_FALSE)
|
||||
self.add_product_flag(condition=conditions.FlagBase.DISABLE_IF_FALSE)
|
||||
self.add_category_flag(condition=conditions.FlagBase.DISABLE_IF_FALSE)
|
||||
|
||||
cart_1 = TestingCartController.for_user(self.USER_1)
|
||||
# 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)
|
||||
|
||||
def test_eit_and_dif_conditions_work_together(self):
|
||||
self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
|
||||
self.add_category_flag(condition=rego.FlagBase.DISABLE_IF_FALSE)
|
||||
self.add_product_flag(condition=conditions.FlagBase.ENABLE_IF_TRUE)
|
||||
self.add_category_flag(condition=conditions.FlagBase.DISABLE_IF_FALSE)
|
||||
|
||||
cart_1 = TestingCartController.for_user(self.USER_1)
|
||||
# Cannot add PROD_1 until both conditions are met
|
||||
|
@ -200,7 +204,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
self.assertTrue(self.PROD_4 in prods)
|
||||
|
||||
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(
|
||||
self.USER_1,
|
||||
|
@ -211,7 +215,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
self.assertTrue(self.PROD_2 in prods)
|
||||
|
||||
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.add_to_cart(self.PROD_2, 1)
|
||||
|
@ -225,7 +229,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
self.assertTrue(self.PROD_2 in prods)
|
||||
|
||||
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(
|
||||
self.USER_1,
|
||||
|
@ -236,7 +240,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
self.assertTrue(self.PROD_2 in prods)
|
||||
|
||||
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.add_to_cart(self.PROD_2, 1)
|
||||
|
@ -250,7 +254,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
self.assertTrue(self.PROD_2 in prods)
|
||||
|
||||
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.add_to_cart(self.PROD_3, 1)
|
||||
|
@ -268,7 +272,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
cart_2.set_quantity(self.PROD_1, 1)
|
||||
|
||||
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.add_to_cart(self.PROD_2, 1)
|
||||
|
@ -286,7 +290,9 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
cart_2.set_quantity(self.PROD_1, 1)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -307,7 +313,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
self.assertTrue(self.CAT_2 in cats)
|
||||
|
||||
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.add_to_cart(self.PROD_2, 1)
|
||||
|
@ -332,7 +338,7 @@ class FlagTestCases(RegistrationCartTestCase):
|
|||
cart.validate_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])
|
||||
|
||||
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
|
||||
# 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])
|
||||
|
|
|
@ -4,7 +4,9 @@ import pytz
|
|||
from decimal import Decimal
|
||||
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 TestingCreditNoteController
|
||||
from controller_helpers import TestingInvoiceController
|
||||
|
@ -23,7 +25,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
current_cart.add_to_cart(self.PROD_1, 1)
|
||||
invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
|
||||
# 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))
|
||||
# That invoice should have a value equal to cost of PROD_1
|
||||
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)
|
||||
|
||||
# The old invoice should automatically be voided
|
||||
invoice_1_new = rego.Invoice.objects.get(pk=invoice_1.invoice.id)
|
||||
invoice_2_new = rego.Invoice.objects.get(pk=invoice_2.invoice.id)
|
||||
invoice_1_new = commerce.Invoice.objects.get(pk=invoice_1.invoice.id)
|
||||
invoice_2_new = commerce.Invoice.objects.get(pk=invoice_2.invoice.id)
|
||||
self.assertTrue(invoice_1_new.is_void)
|
||||
self.assertFalse(invoice_2_new.is_void)
|
||||
|
||||
# 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))
|
||||
# Invoice should have a value equal to cost of PROD_1 and PROD_2
|
||||
self.assertEqual(
|
||||
|
@ -79,16 +85,16 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
self.assertNotEqual(current_cart.cart, new_cart.cart)
|
||||
|
||||
def test_invoice_includes_discounts(self):
|
||||
voucher = rego.Voucher.objects.create(
|
||||
voucher = inventory.Voucher.objects.create(
|
||||
recipient="Voucher recipient",
|
||||
code="VOUCHER",
|
||||
limit=1
|
||||
)
|
||||
discount = rego.VoucherDiscount.objects.create(
|
||||
discount = conditions.VoucherDiscount.objects.create(
|
||||
description="VOUCHER RECIPIENT",
|
||||
voucher=voucher,
|
||||
)
|
||||
rego.DiscountForProduct.objects.create(
|
||||
conditions.DiscountForProduct.objects.create(
|
||||
discount=discount,
|
||||
product=self.PROD_1,
|
||||
percentage=Decimal(50),
|
||||
|
@ -103,7 +109,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
|
||||
|
||||
# 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))
|
||||
# That invoice should have a value equal to 50% of the cost of PROD_1
|
||||
self.assertEqual(
|
||||
|
@ -111,16 +119,16 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
invoice_1.invoice.value)
|
||||
|
||||
def test_zero_value_invoice_is_automatically_paid(self):
|
||||
voucher = rego.Voucher.objects.create(
|
||||
voucher = inventory.Voucher.objects.create(
|
||||
recipient="Voucher recipient",
|
||||
code="VOUCHER",
|
||||
limit=1
|
||||
)
|
||||
discount = rego.VoucherDiscount.objects.create(
|
||||
discount = conditions.VoucherDiscount.objects.create(
|
||||
description="VOUCHER RECIPIENT",
|
||||
voucher=voucher,
|
||||
)
|
||||
rego.DiscountForProduct.objects.create(
|
||||
conditions.DiscountForProduct.objects.create(
|
||||
discount=discount,
|
||||
product=self.PROD_1,
|
||||
percentage=Decimal(100),
|
||||
|
@ -239,7 +247,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
self.assertTrue(invoice.invoice.is_paid)
|
||||
|
||||
# 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(to_pay - invoice.invoice.value, credit_notes[0].value)
|
||||
|
||||
|
@ -257,7 +267,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
self.assertTrue(invoice.invoice.is_paid)
|
||||
|
||||
# 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())
|
||||
|
||||
def test_refund_partially_paid_invoice_generates_correct_credit_note(self):
|
||||
|
@ -276,7 +288,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
self.assertTrue(invoice.invoice.is_void)
|
||||
|
||||
# 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(to_pay, credit_notes[0].value)
|
||||
|
||||
|
@ -297,7 +311,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
self.assertTrue(invoice.invoice.is_refunded)
|
||||
|
||||
# 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(to_pay, credit_notes[0].value)
|
||||
|
||||
|
@ -314,11 +330,11 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
invoice.refund()
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
cart = TestingCartController.for_user(self.USER_1)
|
||||
|
@ -330,7 +346,7 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
self.assertTrue(invoice2.invoice.is_paid)
|
||||
|
||||
# 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):
|
||||
cart = TestingCartController.for_user(self.USER_1)
|
||||
|
@ -345,10 +361,10 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
invoice.refund()
|
||||
|
||||
# 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)
|
||||
|
||||
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
|
||||
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,
|
||||
# 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
|
||||
# minus the cost of cart 2.
|
||||
|
@ -385,7 +403,7 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
invoice.refund()
|
||||
|
||||
# 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)
|
||||
|
||||
# Create a new cart with invoice, pay it
|
||||
|
@ -426,15 +444,15 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
|
||||
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.refund()
|
||||
|
||||
# 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
|
||||
cart = TestingCartController.for_user(self.USER_1)
|
||||
|
@ -458,9 +476,9 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
|
||||
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)
|
||||
|
||||
|
@ -471,7 +489,7 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
|||
invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart))
|
||||
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.
|
||||
with self.assertRaises(ValidationError):
|
||||
|
|
|
@ -5,7 +5,8 @@ from decimal import Decimal
|
|||
from django.core.exceptions import ValidationError
|
||||
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 TestingInvoiceController
|
||||
|
||||
|
@ -32,14 +33,14 @@ class VoucherTestCases(RegistrationCartTestCase):
|
|||
|
||||
# After the reservation duration
|
||||
# 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.next_cart()
|
||||
|
||||
# After the reservation duration, even though the voucher has applied,
|
||||
# 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):
|
||||
cart_1.validate_cart()
|
||||
|
||||
|
@ -58,10 +59,10 @@ class VoucherTestCases(RegistrationCartTestCase):
|
|||
def test_voucher_enables_item(self):
|
||||
voucher = self.new_voucher()
|
||||
|
||||
flag = rego.VoucherFlag.objects.create(
|
||||
flag = conditions.VoucherFlag.objects.create(
|
||||
description="Voucher condition",
|
||||
voucher=voucher,
|
||||
condition=rego.FlagBase.ENABLE_IF_TRUE,
|
||||
condition=conditions.FlagBase.ENABLE_IF_TRUE,
|
||||
)
|
||||
flag.save()
|
||||
flag.products.add(self.PROD_1)
|
||||
|
@ -79,12 +80,12 @@ class VoucherTestCases(RegistrationCartTestCase):
|
|||
def test_voucher_enables_discount(self):
|
||||
voucher = self.new_voucher()
|
||||
|
||||
discount = rego.VoucherDiscount.objects.create(
|
||||
discount = conditions.VoucherDiscount.objects.create(
|
||||
description="VOUCHER RECIPIENT",
|
||||
voucher=voucher,
|
||||
)
|
||||
discount.save()
|
||||
rego.DiscountForProduct.objects.create(
|
||||
conditions.DiscountForProduct.objects.create(
|
||||
discount=discount,
|
||||
product=self.PROD_1,
|
||||
percentage=Decimal(100),
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import sys
|
||||
|
||||
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.cart import CartController
|
||||
from registrasion.controllers.credit_note import CreditNoteController
|
||||
|
@ -60,7 +62,7 @@ def guided_registration(request, page_id=0):
|
|||
|
||||
sections = []
|
||||
|
||||
attendee = rego.Attendee.get_instance(request.user)
|
||||
attendee = people.Attendee.get_instance(request.user)
|
||||
|
||||
if attendee.completed_registration:
|
||||
return render(
|
||||
|
@ -111,7 +113,7 @@ def guided_registration(request, page_id=0):
|
|||
starting = attendee.guided_categories_complete.count() == 0
|
||||
|
||||
# Get the next category
|
||||
cats = rego.Category.objects
|
||||
cats = inventory.Category.objects
|
||||
if SESSION_KEY in request.session:
|
||||
_cats = request.session[SESSION_KEY]
|
||||
cats = cats.filter(id__in=_cats)
|
||||
|
@ -134,7 +136,7 @@ def guided_registration(request, page_id=0):
|
|||
current_step = 3
|
||||
title = "Additional items"
|
||||
|
||||
all_products = rego.Product.objects.filter(
|
||||
all_products = inventory.Product.objects.filter(
|
||||
category__in=cats,
|
||||
).select_related("category")
|
||||
|
||||
|
@ -217,11 +219,13 @@ def edit_profile(request):
|
|||
def handle_profile(request, prefix):
|
||||
''' Returns a profile form instance, and a boolean which is true if the
|
||||
form was handled. '''
|
||||
attendee = rego.Attendee.get_instance(request.user)
|
||||
attendee = people.Attendee.get_instance(request.user)
|
||||
|
||||
try:
|
||||
profile = attendee.attendeeprofilebase
|
||||
profile = rego.AttendeeProfileBase.objects.get_subclass(pk=profile.id)
|
||||
profile = people.AttendeeProfileBase.objects.get_subclass(
|
||||
pk=profile.id,
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
profile = None
|
||||
|
||||
|
@ -270,7 +274,7 @@ def product_category(request, category_id):
|
|||
voucher_form, voucher_handled = v
|
||||
|
||||
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(
|
||||
request.user,
|
||||
|
@ -316,7 +320,7 @@ def handle_products(request, category, products, prefix):
|
|||
ProductsForm = forms.ProductsForm(category, products)
|
||||
|
||||
# Create initial data for each of products in category
|
||||
items = rego.ProductItem.objects.filter(
|
||||
items = commerce.ProductItem.objects.filter(
|
||||
product__in=products,
|
||||
cart=current_cart.cart,
|
||||
).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
|
||||
# in an active+valid cart
|
||||
if category.required:
|
||||
carts = rego.Cart.objects.filter(user=request.user)
|
||||
items = rego.ProductItem.objects.filter(
|
||||
carts = commerce.Cart.objects.filter(user=request.user)
|
||||
items = commerce.ProductItem.objects.filter(
|
||||
product__category=category,
|
||||
cart=carts,
|
||||
)
|
||||
|
@ -366,7 +370,7 @@ def set_quantities_from_products_form(products_form, current_cart):
|
|||
quantities = list(products_form.product_quantities())
|
||||
|
||||
pks = [i[0] for i in quantities]
|
||||
products = rego.Product.objects.filter(
|
||||
products = inventory.Product.objects.filter(
|
||||
id__in=pks,
|
||||
).select_related("category")
|
||||
|
||||
|
@ -384,7 +388,7 @@ def set_quantities_from_products_form(products_form, current_cart):
|
|||
product, message = ve_field.message
|
||||
if product in field_names:
|
||||
field = field_names[product]
|
||||
elif isinstance(product, rego.Product):
|
||||
elif isinstance(product, inventory.Product):
|
||||
continue
|
||||
else:
|
||||
field = None
|
||||
|
@ -402,7 +406,7 @@ def handle_voucher(request, prefix):
|
|||
voucher_form.cleaned_data["voucher"].strip()):
|
||||
|
||||
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:
|
||||
# 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
|
||||
the given access code, if any. '''
|
||||
|
||||
invoices = rego.Invoice.objects.filter(
|
||||
invoices = commerce.Invoice.objects.filter(
|
||||
user__attendee__access_code=access_code,
|
||||
status=rego.Invoice.STATUS_UNPAID,
|
||||
status=commerce.Invoice.STATUS_UNPAID,
|
||||
).order_by("issue_time")
|
||||
|
||||
if not invoices:
|
||||
|
@ -478,7 +482,7 @@ def invoice(request, invoice_id, access_code=None):
|
|||
'''
|
||||
|
||||
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)
|
||||
|
||||
|
@ -505,7 +509,7 @@ def manual_payment(request, invoice_id):
|
|||
raise Http404()
|
||||
|
||||
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)
|
||||
|
||||
form = forms.ManualPaymentForm(
|
||||
|
@ -536,7 +540,7 @@ def refund(request, invoice_id):
|
|||
raise Http404()
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
|
@ -557,7 +561,7 @@ def credit_note(request, note_id, access_code=None):
|
|||
raise Http404()
|
||||
|
||||
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)
|
||||
|
||||
|
@ -574,10 +578,11 @@ def credit_note(request, note_id, access_code=None):
|
|||
|
||||
if request.POST and apply_form.is_valid():
|
||||
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)
|
||||
messages.success(request,
|
||||
"Applied credit note %d to invoice." % note_id
|
||||
messages.success(
|
||||
request,
|
||||
"Applied credit note %d to invoice." % note_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.parent = note
|
||||
refund_form.save()
|
||||
messages.success(request,
|
||||
messages.success(
|
||||
request,
|
||||
"Applied manual refund to credit note."
|
||||
)
|
||||
return redirect("invoice", invoice.id)
|
||||
|
|
Loading…
Reference in a new issue