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…
Reference in a new issue