diff --git a/registrasion/controllers/item.py b/registrasion/controllers/item.py index a36604e5..f2d0a2ae 100644 --- a/registrasion/controllers/item.py +++ b/registrasion/controllers/item.py @@ -30,7 +30,7 @@ class ItemController(object): def __init__(self, user): self.user = user - def items_purchased(self, category=None): + def _items(self, cart_status, category=None): ''' Aggregates the items that this user has purchased. Arguments: @@ -45,7 +45,7 @@ class ItemController(object): in_cart = ( Q(productitem__cart__user=self.user) & - Q(productitem__cart__status=commerce.Cart.STATUS_PAID) + Q(productitem__cart__status=cart_status) ) quantities_in_cart = When( @@ -72,6 +72,20 @@ class ItemController(object): out.append(ProductAndQuantity(prod, prod.quantity)) return out + def items_purchased(self, category=None): + ''' Aggregates the items that this user has purchased. + + Arguments: + category (Optional[models.inventory.Category]): the category + of items to restrict to. + + Returns: + [ProductAndQuantity, ...]: A list of product-quantity pairs, + aggregating like products from across multiple invoices. + + ''' + return self._items(commerce.Cart.STATUS_PAID) + def items_pending(self): ''' Gets all of the items that the user has reserved, but has not yet paid for. @@ -82,14 +96,16 @@ class ItemController(object): ''' - all_items = commerce.ProductItem.objects.filter( - cart__user=self.user, - cart__status=commerce.Cart.STATUS_ACTIVE, - ).select_related( - "product", - "product__category", - ).order_by( - "product__category__order", - "product__order", - ) - return all_items + return self._items(commerce.Cart.STATUS_ACTIVE) + + def items_released(self): + ''' Gets all of the items that the user previously paid for, but has + since refunded. + + Returns: + [ProductAndQuantity, ...]: A list of product-quantity pairs for the + items that the user has not yet paid for. + + ''' + + return self._items(commerce.Cart.STATUS_RELEASED) diff --git a/registrasion/forms.py b/registrasion/forms.py index d6e7878e..90bf1b48 100644 --- a/registrasion/forms.py +++ b/registrasion/forms.py @@ -1,3 +1,4 @@ +from registrasion.controllers.product import ProductController from registrasion.models import commerce from registrasion.models import inventory @@ -347,3 +348,33 @@ class VoucherForm(forms.Form): help_text="If you have a voucher code, enter it here", required=False, ) + + +def staff_products_form_factory(user): + ''' Creates a StaffProductsForm that restricts the available products to + those that are available to a user. ''' + + products = inventory.Product.objects.all() + products = ProductController.available_products(user, products=products) + + product_ids = [product.id for product in products] + product_set = inventory.Product.objects.filter(id__in=product_ids) + + class StaffProductsForm(forms.Form): + ''' Form for allowing staff to add an item to a user's cart. ''' + + product = forms.ModelChoiceField( + widget=forms.Select, + queryset=product_set, + ) + + quantity = forms.IntegerField( + min_value=0, + ) + + return StaffProductsForm + +def staff_products_formset_factory(user): + ''' Creates a formset of StaffProductsForm for the given user. ''' + form_type = staff_products_form_factory(user) + return forms.formset_factory(form_type) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index eb512454..6a507135 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -236,17 +236,17 @@ def credit_notes(request, form): @report_view("Attendee", form_type=forms.UserIdForm) -def attendee(request, form, attendee_id=None): +def attendee(request, form, user_id=None): ''' Returns a list of all manifested attendees if no attendee is specified, else displays the attendee manifest. ''' - if attendee_id is None and not form.has_changed(): + if user_id is None and not form.has_changed(): return attendee_list(request) if form.cleaned_data["user"] is not None: - attendee_id = form.cleaned_data["user"] + user_id = form.cleaned_data["user"] - attendee = people.Attendee.objects.get(id=attendee_id) + attendee = people.Attendee.objects.get(user__id=user_id) reports = [] @@ -349,7 +349,7 @@ def attendee_list(request): for attendee in attendees: data.append([ - attendee.id, + attendee.user.id, attendee.attendeeprofilebase.attendee_name(), attendee.user.email, attendee.has_registered > 0, diff --git a/registrasion/urls.py b/registrasion/urls.py index ae6ac8a1..ac7faace 100644 --- a/registrasion/urls.py +++ b/registrasion/urls.py @@ -13,12 +13,15 @@ from .views import ( invoice_access, edit_profile, guided_registration, + amend_registration, ) public = [ + url(r"^amend/([0-9]+)$", amend_registration, name="amend_registration"), url(r"^category/([0-9]+)$", product_category, name="product_category"), url(r"^checkout$", checkout, name="checkout"), + url(r"^checkout/([0-9]+)$", checkout, name="checkout"), url(r"^credit_note/([0-9]+)$", credit_note, name="credit_note"), url(r"^invoice/([0-9]+)$", invoice, name="invoice"), url(r"^invoice/([0-9]+)/([A-Z0-9]+)$", invoice, name="invoice"), diff --git a/registrasion/views.py b/registrasion/views.py index 95e97793..c457295f 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -10,6 +10,7 @@ from registrasion.controllers.cart import CartController from registrasion.controllers.credit_note import CreditNoteController from registrasion.controllers.discount import DiscountController from registrasion.controllers.invoice import InvoiceController +from registrasion.controllers.item import ItemController from registrasion.controllers.product import ProductController from registrasion.exceptions import CartValidationError @@ -18,6 +19,7 @@ from collections import namedtuple from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import user_passes_test +from django.contrib.auth.models import User from django.contrib import messages from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError @@ -504,7 +506,7 @@ def _handle_voucher(request, prefix): @login_required -def checkout(request): +def checkout(request, user_id=None): ''' Runs the checkout process for the current cart. If the query string contains ``fix_errors=true``, Registrasion will attempt @@ -512,6 +514,10 @@ def checkout(request): cancelling expired discounts and vouchers, and removing any unavailable products. + Arguments: + user_id (castable to int): + If the requesting user is staff, then the user ID can be used to + run checkout for another user. Returns: render or redirect: If the invoice is generated successfully, or there's already a @@ -525,7 +531,15 @@ def checkout(request): ''' - current_cart = CartController.for_user(request.user) + if user_id is not None: + if request.user.is_staff: + user = User.objects.get(id=int(user_id)) + else: + raise Http404() + else: + user = request.user + + current_cart = CartController.for_user(user) if "fix_errors" in request.GET and request.GET["fix_errors"] == "true": current_cart.fix_simple_errors() @@ -790,3 +804,66 @@ def credit_note(request, note_id, access_code=None): } return render(request, "registrasion/credit_note.html", data) + + +@user_passes_test(_staff_only) +def amend_registration(request, user_id): + ''' Allows staff to amend a user's current registration cart, and etc etc. + ''' + + user = User.objects.get(id=int(user_id)) + current_cart = CartController.for_user(user) + + items = commerce.ProductItem.objects.filter( + cart=current_cart.cart, + ).select_related("product") + initial = [{"product": i.product, "quantity": i.quantity} for i in items] + + StaffProductsFormSet = forms.staff_products_formset_factory(user) + formset = StaffProductsFormSet( + request.POST or None, + initial=initial, + prefix="products", + ) + + voucher_form = forms.VoucherForm( + request.POST or None, + prefix="voucher", + ) + + if request.POST and formset.is_valid(): + + pq = [ + (f.cleaned_data["product"], f.cleaned_data["quantity"]) + for f in formset + if "product" in f.cleaned_data and + f.cleaned_data["product"] is not None + ] + + try: + current_cart.set_quantities(pq) + return redirect(amend_registration, user_id) + except ValidationError as ve: + for ve_field in ve.error_list: + product, message = ve_field.message + for form in formset: + if form.cleaned_data["product"] == product: + form.add_error("quantity", message) + + if request.POST and voucher_form.is_valid(): + try: + current_cart.apply_voucher(voucher_form.cleaned_data["voucher"]) + return redirect(amend_registration, user_id) + except ValidationError as ve: + voucher_form.add_error(None, ve) + + ic = ItemController(user) + data = { + "user": user, + "paid": ic.items_purchased(), + "cancelled": ic.items_released(), + "form": formset, + "voucher_form": voucher_form, + } + + return render(request, "registrasion/amend_registration.html", data)