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 datetime | ||||
| import discount | ||||
| import functools | ||||
| import itertools | ||||
| 
 | ||||
| from django.core.exceptions import ObjectDoesNotExist | ||||
|  | @ -19,6 +20,18 @@ from conditions import ConditionController | |||
| 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): | ||||
| 
 | ||||
|     def __init__(self, cart): | ||||
|  | @ -42,6 +55,12 @@ class CartController(object): | |||
|             ) | ||||
|         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): | ||||
|         ''' Updates the cart's time last updated value, which is used to | ||||
|         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.reservation_duration = max(reservations) | ||||
| 
 | ||||
|     @_modifies_cart | ||||
|     def end_batch(self): | ||||
|         ''' Performs operations that occur occur at the end of a batch of | ||||
|         product changes/voucher applications etc. | ||||
|  | @ -76,6 +96,7 @@ class CartController(object): | |||
|         self.cart.revision += 1 | ||||
|         self.cart.save() | ||||
| 
 | ||||
|     @_modifies_cart | ||||
|     @transaction.atomic | ||||
|     def set_quantities(self, product_quantities): | ||||
|         ''' Sets the quantities on each of the products on each of the | ||||
|  | @ -176,6 +197,7 @@ class CartController(object): | |||
|         if errors: | ||||
|             raise CartValidationError(errors) | ||||
| 
 | ||||
|     @_modifies_cart | ||||
|     def apply_voucher(self, voucher_code): | ||||
|         ''' Applies the voucher with the given code to this cart. ''' | ||||
| 
 | ||||
|  | @ -229,6 +251,37 @@ class CartController(object): | |||
|         if errors: | ||||
|             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): | ||||
|         ''' Determines whether the status of the current cart is valid; | ||||
|         this is normally called before generating or paying an invoice ''' | ||||
|  | @ -248,8 +301,12 @@ class CartController(object): | |||
|         try: | ||||
|             self._test_limits(product_quantities) | ||||
|         except ValidationError as ve: | ||||
|             for error in ve.error_list: | ||||
|                 errors.append(error.message[1]) | ||||
|             self._append_errors(errors, ve) | ||||
| 
 | ||||
|         try: | ||||
|             self._test_required_categories() | ||||
|         except ValidationError as ve: | ||||
|             self._append_errors(errors, ve) | ||||
| 
 | ||||
|         # Validate the discounts | ||||
|         discount_items = commerce.DiscountItem.objects.filter(cart=cart) | ||||
|  | @ -272,6 +329,7 @@ class CartController(object): | |||
|         if errors: | ||||
|             raise ValidationError(errors) | ||||
| 
 | ||||
|     @_modifies_cart | ||||
|     @transaction.atomic | ||||
|     def fix_simple_errors(self): | ||||
|         ''' This attempts to fix the easy errors raised by ValidationError. | ||||
|  | @ -304,6 +362,7 @@ class CartController(object): | |||
| 
 | ||||
|         self.set_quantities(zeros) | ||||
| 
 | ||||
|     @_modifies_cart | ||||
|     @transaction.atomic | ||||
|     def recalculate_discounts(self): | ||||
|         ''' Calculates all of the discounts available for this product. | ||||
|  |  | |||
|  | @ -100,6 +100,7 @@ class _QuantityBoxProductsForm(_ProductsForm): | |||
|                 label=product.name, | ||||
|                 help_text=help_text, | ||||
|                 min_value=0, | ||||
|                 max_value=500,  # Issue #19. We should figure out real limit. | ||||
|             ) | ||||
|             cls.base_fields[cls.field_name(product)] = field | ||||
| 
 | ||||
|  | @ -132,6 +133,9 @@ class _RadioButtonProductsForm(_ProductsForm): | |||
|             choice_text = "%s -- $%d" % (product.name, product.price) | ||||
|             choices.append((product.id, choice_text)) | ||||
| 
 | ||||
|         if not category.required: | ||||
|             choices.append((0, "No selection")) | ||||
| 
 | ||||
|         cls.base_fields[cls.FIELD] = forms.TypedChoiceField( | ||||
|             label=category.name, | ||||
|             widget=forms.RadioSelect, | ||||
|  | @ -155,6 +159,8 @@ class _RadioButtonProductsForm(_ProductsForm): | |||
|         ours = self.cleaned_data[self.FIELD] | ||||
|         choices = self.fields[self.FIELD].choices | ||||
|         for choice_value, choice_display in choices: | ||||
|             if choice_value == 0: | ||||
|                 continue | ||||
|             yield ( | ||||
|                 choice_value, | ||||
|                 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: | ||||
|         app_label = "registrasion" | ||||
| 
 | ||||
|     entered_by = models.ForeignKey(User) | ||||
| 
 | ||||
| 
 | ||||
| class CreditNote(PaymentBase): | ||||
|     ''' Credit notes represent money accounted for in the system that do not | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ class TestingInvoiceController(InvoiceController): | |||
|             self.validate_allowed_to_pay() | ||||
| 
 | ||||
|         ''' Adds a payment ''' | ||||
|         commerce.ManualPayment.objects.create( | ||||
|         commerce.PaymentBase.objects.create( | ||||
|             invoice=self.invoice, | ||||
|             reference=reference, | ||||
|             amount=amount, | ||||
|  |  | |||
|  | @ -499,3 +499,37 @@ class InvoiceTestCase(RegistrationCartTestCase): | |||
|         invoice.pay("Paying into the void.", val, pre_validate=False) | ||||
|         cn = self._credit_note_for_invoice(invoice.invoice) | ||||
|         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): | ||||
|     ''' Redirects to the first unpaid invoice for the attendee that matches | ||||
|     the given access code, if any. | ||||
|     ''' Redirects to an invoice for the attendee that matches the given access | ||||
|     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: | ||||
| 
 | ||||
|  | @ -552,21 +558,29 @@ def invoice_access(request, access_code): | |||
| 
 | ||||
|     Returns: | ||||
|         redirect: | ||||
|             Redirect to the first unpaid invoice for that user. | ||||
|             Redirect to the selected invoice for that user. | ||||
| 
 | ||||
|     Raises: | ||||
|         Http404: If there is no such invoice. | ||||
|         Http404: If the user has no invoices. | ||||
|     ''' | ||||
| 
 | ||||
|     invoices = commerce.Invoice.objects.filter( | ||||
|         user__attendee__access_code=access_code, | ||||
|         status=commerce.Invoice.STATUS_UNPAID, | ||||
|     ).order_by("issue_time") | ||||
|     ).order_by("-issue_time") | ||||
| 
 | ||||
| 
 | ||||
|     if not invoices: | ||||
|         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) | ||||
| 
 | ||||
|  | @ -655,6 +669,7 @@ def manual_payment(request, invoice_id): | |||
| 
 | ||||
|     if request.POST and form.is_valid(): | ||||
|         form.instance.invoice = inv | ||||
|         form.instance.entered_by = request.user | ||||
|         form.save() | ||||
|         current_invoice.update_status() | ||||
|         form = forms.ManualPaymentForm(prefix=FORM_PREFIX) | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer