Merge branch 'random_bug_fixes'
This commit is contained in:
		
						commit
						e540d6a815
					
				
					 7 changed files with 150 additions and 10 deletions
				
			
		|  | @ -1,6 +1,7 @@ | ||||||
| import collections | import collections | ||||||
| import datetime | import datetime | ||||||
| import discount | import discount | ||||||
|  | import functools | ||||||
| import itertools | import itertools | ||||||
| 
 | 
 | ||||||
| from django.core.exceptions import ObjectDoesNotExist | from django.core.exceptions import ObjectDoesNotExist | ||||||
|  | @ -19,6 +20,18 @@ from conditions import ConditionController | ||||||
| from product import ProductController | from product import ProductController | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def _modifies_cart(func): | ||||||
|  |     ''' Decorator that makes the wrapped function raise ValidationError | ||||||
|  |     if we're doing something that could modify the cart. ''' | ||||||
|  | 
 | ||||||
|  |     @functools.wraps(func) | ||||||
|  |     def inner(self, *a, **k): | ||||||
|  |         self._fail_if_cart_is_not_active() | ||||||
|  |         return func(self, *a, **k) | ||||||
|  | 
 | ||||||
|  |     return inner | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class CartController(object): | class CartController(object): | ||||||
| 
 | 
 | ||||||
|     def __init__(self, cart): |     def __init__(self, cart): | ||||||
|  | @ -42,6 +55,12 @@ class CartController(object): | ||||||
|             ) |             ) | ||||||
|         return cls(existing) |         return cls(existing) | ||||||
| 
 | 
 | ||||||
|  |     def _fail_if_cart_is_not_active(self): | ||||||
|  |         self.cart.refresh_from_db() | ||||||
|  |         if self.cart.status != commerce.Cart.STATUS_ACTIVE: | ||||||
|  |             raise ValidationError("You can only amend active carts.") | ||||||
|  | 
 | ||||||
|  |     @_modifies_cart | ||||||
|     def extend_reservation(self): |     def extend_reservation(self): | ||||||
|         ''' Updates the cart's time last updated value, which is used to |         ''' Updates the cart's time last updated value, which is used to | ||||||
|         determine whether the cart has reserved the items and discounts it |         determine whether the cart has reserved the items and discounts it | ||||||
|  | @ -64,6 +83,7 @@ class CartController(object): | ||||||
|         self.cart.time_last_updated = timezone.now() |         self.cart.time_last_updated = timezone.now() | ||||||
|         self.cart.reservation_duration = max(reservations) |         self.cart.reservation_duration = max(reservations) | ||||||
| 
 | 
 | ||||||
|  |     @_modifies_cart | ||||||
|     def end_batch(self): |     def end_batch(self): | ||||||
|         ''' Performs operations that occur occur at the end of a batch of |         ''' Performs operations that occur occur at the end of a batch of | ||||||
|         product changes/voucher applications etc. |         product changes/voucher applications etc. | ||||||
|  | @ -76,6 +96,7 @@ class CartController(object): | ||||||
|         self.cart.revision += 1 |         self.cart.revision += 1 | ||||||
|         self.cart.save() |         self.cart.save() | ||||||
| 
 | 
 | ||||||
|  |     @_modifies_cart | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def set_quantities(self, product_quantities): |     def set_quantities(self, product_quantities): | ||||||
|         ''' Sets the quantities on each of the products on each of the |         ''' Sets the quantities on each of the products on each of the | ||||||
|  | @ -176,6 +197,7 @@ class CartController(object): | ||||||
|         if errors: |         if errors: | ||||||
|             raise CartValidationError(errors) |             raise CartValidationError(errors) | ||||||
| 
 | 
 | ||||||
|  |     @_modifies_cart | ||||||
|     def apply_voucher(self, voucher_code): |     def apply_voucher(self, voucher_code): | ||||||
|         ''' Applies the voucher with the given code to this cart. ''' |         ''' Applies the voucher with the given code to this cart. ''' | ||||||
| 
 | 
 | ||||||
|  | @ -229,6 +251,37 @@ class CartController(object): | ||||||
|         if errors: |         if errors: | ||||||
|             raise(ValidationError(ve)) |             raise(ValidationError(ve)) | ||||||
| 
 | 
 | ||||||
|  |     def _test_required_categories(self): | ||||||
|  |         ''' Makes sure that the owner of this cart has satisfied all of the | ||||||
|  |         required category constraints in the inventory (be it in this cart | ||||||
|  |         or others). ''' | ||||||
|  | 
 | ||||||
|  |         required = set(inventory.Category.objects.filter(required=True)) | ||||||
|  | 
 | ||||||
|  |         items = commerce.ProductItem.objects.filter( | ||||||
|  |             product__category__required=True, | ||||||
|  |             cart__user=self.cart.user, | ||||||
|  |         ).exclude( | ||||||
|  |             cart__status=commerce.Cart.STATUS_RELEASED, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         for item in items: | ||||||
|  |             print item | ||||||
|  |             required.remove(item.product.category) | ||||||
|  | 
 | ||||||
|  |         errors = [] | ||||||
|  |         for category in required: | ||||||
|  |             msg = "You must have at least one item from: %s" % category | ||||||
|  |             errors.append((None, msg)) | ||||||
|  | 
 | ||||||
|  |         if errors: | ||||||
|  |             raise ValidationError(errors) | ||||||
|  | 
 | ||||||
|  |     def _append_errors(self, errors, ve): | ||||||
|  |         for error in ve.error_list: | ||||||
|  |             print error.message | ||||||
|  |             errors.append(error.message[1]) | ||||||
|  | 
 | ||||||
|     def validate_cart(self): |     def validate_cart(self): | ||||||
|         ''' Determines whether the status of the current cart is valid; |         ''' Determines whether the status of the current cart is valid; | ||||||
|         this is normally called before generating or paying an invoice ''' |         this is normally called before generating or paying an invoice ''' | ||||||
|  | @ -248,8 +301,12 @@ class CartController(object): | ||||||
|         try: |         try: | ||||||
|             self._test_limits(product_quantities) |             self._test_limits(product_quantities) | ||||||
|         except ValidationError as ve: |         except ValidationError as ve: | ||||||
|             for error in ve.error_list: |             self._append_errors(errors, ve) | ||||||
|                 errors.append(error.message[1]) | 
 | ||||||
|  |         try: | ||||||
|  |             self._test_required_categories() | ||||||
|  |         except ValidationError as ve: | ||||||
|  |             self._append_errors(errors, ve) | ||||||
| 
 | 
 | ||||||
|         # Validate the discounts |         # Validate the discounts | ||||||
|         discount_items = commerce.DiscountItem.objects.filter(cart=cart) |         discount_items = commerce.DiscountItem.objects.filter(cart=cart) | ||||||
|  | @ -272,6 +329,7 @@ class CartController(object): | ||||||
|         if errors: |         if errors: | ||||||
|             raise ValidationError(errors) |             raise ValidationError(errors) | ||||||
| 
 | 
 | ||||||
|  |     @_modifies_cart | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def fix_simple_errors(self): |     def fix_simple_errors(self): | ||||||
|         ''' This attempts to fix the easy errors raised by ValidationError. |         ''' This attempts to fix the easy errors raised by ValidationError. | ||||||
|  | @ -304,6 +362,7 @@ class CartController(object): | ||||||
| 
 | 
 | ||||||
|         self.set_quantities(zeros) |         self.set_quantities(zeros) | ||||||
| 
 | 
 | ||||||
|  |     @_modifies_cart | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def recalculate_discounts(self): |     def recalculate_discounts(self): | ||||||
|         ''' Calculates all of the discounts available for this product. |         ''' Calculates all of the discounts available for this product. | ||||||
|  |  | ||||||
|  | @ -100,6 +100,7 @@ class _QuantityBoxProductsForm(_ProductsForm): | ||||||
|                 label=product.name, |                 label=product.name, | ||||||
|                 help_text=help_text, |                 help_text=help_text, | ||||||
|                 min_value=0, |                 min_value=0, | ||||||
|  |                 max_value=500,  # Issue #19. We should figure out real limit. | ||||||
|             ) |             ) | ||||||
|             cls.base_fields[cls.field_name(product)] = field |             cls.base_fields[cls.field_name(product)] = field | ||||||
| 
 | 
 | ||||||
|  | @ -132,6 +133,9 @@ class _RadioButtonProductsForm(_ProductsForm): | ||||||
|             choice_text = "%s -- $%d" % (product.name, product.price) |             choice_text = "%s -- $%d" % (product.name, product.price) | ||||||
|             choices.append((product.id, choice_text)) |             choices.append((product.id, choice_text)) | ||||||
| 
 | 
 | ||||||
|  |         if not category.required: | ||||||
|  |             choices.append((0, "No selection")) | ||||||
|  | 
 | ||||||
|         cls.base_fields[cls.FIELD] = forms.TypedChoiceField( |         cls.base_fields[cls.FIELD] = forms.TypedChoiceField( | ||||||
|             label=category.name, |             label=category.name, | ||||||
|             widget=forms.RadioSelect, |             widget=forms.RadioSelect, | ||||||
|  | @ -155,6 +159,8 @@ class _RadioButtonProductsForm(_ProductsForm): | ||||||
|         ours = self.cleaned_data[self.FIELD] |         ours = self.cleaned_data[self.FIELD] | ||||||
|         choices = self.fields[self.FIELD].choices |         choices = self.fields[self.FIELD].choices | ||||||
|         for choice_value, choice_display in choices: |         for choice_value, choice_display in choices: | ||||||
|  |             if choice_value == 0: | ||||||
|  |                 continue | ||||||
|             yield ( |             yield ( | ||||||
|                 choice_value, |                 choice_value, | ||||||
|                 1 if ours == choice_value else 0, |                 1 if ours == choice_value else 0, | ||||||
|  |  | ||||||
							
								
								
									
										24
									
								
								registrasion/migrations/0026_manualpayment_entered_by.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								registrasion/migrations/0026_manualpayment_entered_by.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Generated by Django 1.9.2 on 2016-04-25 06:05 | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | 
 | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import migrations, models | ||||||
|  | import django.db.models.deletion | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|  |         ('registrasion', '0025_auto_20160425_0411'), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='manualpayment', | ||||||
|  |             name='entered_by', | ||||||
|  |             field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), | ||||||
|  |             preserve_default=False, | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
|  | @ -220,6 +220,8 @@ class ManualPayment(PaymentBase): | ||||||
|     class Meta: |     class Meta: | ||||||
|         app_label = "registrasion" |         app_label = "registrasion" | ||||||
| 
 | 
 | ||||||
|  |     entered_by = models.ForeignKey(User) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class CreditNote(PaymentBase): | class CreditNote(PaymentBase): | ||||||
|     ''' Credit notes represent money accounted for in the system that do not |     ''' Credit notes represent money accounted for in the system that do not | ||||||
|  |  | ||||||
|  | @ -45,7 +45,7 @@ class TestingInvoiceController(InvoiceController): | ||||||
|             self.validate_allowed_to_pay() |             self.validate_allowed_to_pay() | ||||||
| 
 | 
 | ||||||
|         ''' Adds a payment ''' |         ''' Adds a payment ''' | ||||||
|         commerce.ManualPayment.objects.create( |         commerce.PaymentBase.objects.create( | ||||||
|             invoice=self.invoice, |             invoice=self.invoice, | ||||||
|             reference=reference, |             reference=reference, | ||||||
|             amount=amount, |             amount=amount, | ||||||
|  |  | ||||||
|  | @ -499,3 +499,37 @@ class InvoiceTestCase(RegistrationCartTestCase): | ||||||
|         invoice.pay("Paying into the void.", val, pre_validate=False) |         invoice.pay("Paying into the void.", val, pre_validate=False) | ||||||
|         cn = self._credit_note_for_invoice(invoice.invoice) |         cn = self._credit_note_for_invoice(invoice.invoice) | ||||||
|         self.assertEqual(val, cn.credit_note.value) |         self.assertEqual(val, cn.credit_note.value) | ||||||
|  | 
 | ||||||
|  |     def test_required_category_constraints_prevent_invoicing(self): | ||||||
|  |         self.CAT_1.required = True | ||||||
|  |         self.CAT_1.save() | ||||||
|  | 
 | ||||||
|  |         cart = TestingCartController.for_user(self.USER_1) | ||||||
|  |         cart.add_to_cart(self.PROD_3, 1) | ||||||
|  | 
 | ||||||
|  |         # CAT_1 is required, we don't have CAT_1 yet | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             invoice = TestingInvoiceController.for_cart(cart.cart) | ||||||
|  | 
 | ||||||
|  |         # Now that we have CAT_1, we can check out the cart | ||||||
|  |         cart.add_to_cart(self.PROD_1, 1) | ||||||
|  |         invoice = TestingInvoiceController.for_cart(cart.cart) | ||||||
|  | 
 | ||||||
|  |         # Paying for the invoice should work fine | ||||||
|  |         invoice.pay("Boop", invoice.invoice.value) | ||||||
|  | 
 | ||||||
|  |         # We have an item in the first cart, so should be able to invoice | ||||||
|  |         # for the second cart, even without CAT_1 in it. | ||||||
|  |         cart = TestingCartController.for_user(self.USER_1) | ||||||
|  |         cart.add_to_cart(self.PROD_3, 1) | ||||||
|  | 
 | ||||||
|  |         invoice2 = TestingInvoiceController.for_cart(cart.cart) | ||||||
|  | 
 | ||||||
|  |         # Void invoice2, and release the first cart | ||||||
|  |         # now we don't have any CAT_1 | ||||||
|  |         invoice2.void() | ||||||
|  |         invoice.refund() | ||||||
|  | 
 | ||||||
|  |         # Now that we don't have CAT_1, we can't checkout this cart | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             invoice = TestingInvoiceController.for_cart(cart.cart) | ||||||
|  |  | ||||||
|  | @ -542,8 +542,14 @@ def _checkout_errors(request, errors): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def invoice_access(request, access_code): | def invoice_access(request, access_code): | ||||||
|     ''' Redirects to the first unpaid invoice for the attendee that matches |     ''' Redirects to an invoice for the attendee that matches the given access | ||||||
|     the given access code, if any. |     code, if any. | ||||||
|  | 
 | ||||||
|  |     If the attendee has multiple invoices, we use the following tie-break: | ||||||
|  | 
 | ||||||
|  |     - If there's an unpaid invoice, show that, otherwise | ||||||
|  |     - If there's a paid invoice, show the most recent one, otherwise | ||||||
|  |     - Show the most recent invoid of all | ||||||
| 
 | 
 | ||||||
|     Arguments: |     Arguments: | ||||||
| 
 | 
 | ||||||
|  | @ -552,21 +558,29 @@ def invoice_access(request, access_code): | ||||||
| 
 | 
 | ||||||
|     Returns: |     Returns: | ||||||
|         redirect: |         redirect: | ||||||
|             Redirect to the first unpaid invoice for that user. |             Redirect to the selected invoice for that user. | ||||||
| 
 | 
 | ||||||
|     Raises: |     Raises: | ||||||
|         Http404: If there is no such invoice. |         Http404: If the user has no invoices. | ||||||
|     ''' |     ''' | ||||||
| 
 | 
 | ||||||
|     invoices = commerce.Invoice.objects.filter( |     invoices = commerce.Invoice.objects.filter( | ||||||
|         user__attendee__access_code=access_code, |         user__attendee__access_code=access_code, | ||||||
|         status=commerce.Invoice.STATUS_UNPAID, |     ).order_by("-issue_time") | ||||||
|     ).order_by("issue_time") | 
 | ||||||
| 
 | 
 | ||||||
|     if not invoices: |     if not invoices: | ||||||
|         raise Http404() |         raise Http404() | ||||||
| 
 | 
 | ||||||
|     invoice = invoices[0] |     unpaid = invoices.filter(status=commerce.Invoice.STATUS_UNPAID) | ||||||
|  |     paid = invoices.filter(status=commerce.Invoice.STATUS_PAID) | ||||||
|  | 
 | ||||||
|  |     if unpaid: | ||||||
|  |         invoice = unpaid[0]  # (should only be 1 unpaid invoice?) | ||||||
|  |     elif paid: | ||||||
|  |         invoice = paid[0]  # Most recent paid invoice | ||||||
|  |     else: | ||||||
|  |         invoice = invoices[0]  # Most recent of any invoices | ||||||
| 
 | 
 | ||||||
|     return redirect("invoice", invoice.id, access_code) |     return redirect("invoice", invoice.id, access_code) | ||||||
| 
 | 
 | ||||||
|  | @ -655,6 +669,7 @@ def manual_payment(request, invoice_id): | ||||||
| 
 | 
 | ||||||
|     if request.POST and form.is_valid(): |     if request.POST and form.is_valid(): | ||||||
|         form.instance.invoice = inv |         form.instance.invoice = inv | ||||||
|  |         form.instance.entered_by = request.user | ||||||
|         form.save() |         form.save() | ||||||
|         current_invoice.update_status() |         current_invoice.update_status() | ||||||
|         form = forms.ManualPaymentForm(prefix=FORM_PREFIX) |         form = forms.ManualPaymentForm(prefix=FORM_PREFIX) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer