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