Consolidates models.py into a directory module.
This commit is contained in:
		
							parent
							
								
									278ca23c29
								
							
						
					
					
						commit
						875f736d67
					
				
					 24 changed files with 1193 additions and 1040 deletions
				
			
		|  | @ -4,7 +4,8 @@ from django.utils.translation import ugettext_lazy as _ | |||
| 
 | ||||
| import nested_admin | ||||
| 
 | ||||
| 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…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer