Merge branch 'random_bug_fixes'

This commit is contained in:
Christopher Neugebauer 2016-04-25 17:14:57 +10:00
commit e540d6a815
7 changed files with 150 additions and 10 deletions

View file

@ -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.

View file

@ -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,

View 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,
),
]

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -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)