diff --git a/registrasion/controllers/item.py b/registrasion/controllers/item.py new file mode 100644 index 00000000..a36604e5 --- /dev/null +++ b/registrasion/controllers/item.py @@ -0,0 +1,95 @@ +''' NEEDS TESTS ''' + +from registrasion.models import commerce +from registrasion.models import inventory + +from collections import namedtuple +from django.db.models import Case +from django.db.models import Q +from django.db.models import Sum +from django.db.models import When +from django.db.models import Value + +_ProductAndQuantity = namedtuple("ProductAndQuantity", ["product", "quantity"]) + + +class ProductAndQuantity(_ProductAndQuantity): + ''' Class that holds a product and a quantity. + + Attributes: + product (models.inventory.Product) + + quantity (int) + + ''' + pass + + +class ItemController(object): + + def __init__(self, user): + self.user = user + + 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. + + ''' + + in_cart = ( + Q(productitem__cart__user=self.user) & + Q(productitem__cart__status=commerce.Cart.STATUS_PAID) + ) + + quantities_in_cart = When( + in_cart, + then="productitem__quantity", + ) + + quantities_or_zero = Case( + quantities_in_cart, + default=Value(0), + ) + + products = inventory.Product.objects + + if category: + products = products.filter(category=category) + + products = products.select_related("category") + products = products.annotate(quantity=Sum(quantities_or_zero)) + products = products.filter(quantity__gt=0) + + out = [] + for prod in products: + out.append(ProductAndQuantity(prod, prod.quantity)) + return out + + def items_pending(self): + ''' Gets all of the items that the user has reserved, but has not yet + paid for. + + Returns: + [ProductAndQuantity, ...]: A list of product-quantity pairs for the + items that the user has not yet paid for. + + ''' + + 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 diff --git a/registrasion/forms.py b/registrasion/forms.py index 2b5ef97b..d6e7878e 100644 --- a/registrasion/forms.py +++ b/registrasion/forms.py @@ -347,16 +347,3 @@ class VoucherForm(forms.Form): help_text="If you have a voucher code, enter it here", required=False, ) - - -# Staff-facing forms. - -class ProductAndCategoryForm(forms.Form): - product = forms.ModelMultipleChoiceField( - queryset=inventory.Product.objects.all(), - required=False, - ) - category = forms.ModelMultipleChoiceField( - queryset=inventory.Category.objects.all(), - required=False, - ) diff --git a/registrasion/reporting/forms.py b/registrasion/reporting/forms.py new file mode 100644 index 00000000..f4902660 --- /dev/null +++ b/registrasion/reporting/forms.py @@ -0,0 +1,22 @@ +from registrasion.models import inventory + +from django import forms + +# Staff-facing forms. + +class ProductAndCategoryForm(forms.Form): + product = forms.ModelMultipleChoiceField( + queryset=inventory.Product.objects.all(), + required=False, + ) + category = forms.ModelMultipleChoiceField( + queryset=inventory.Category.objects.all(), + required=False, + ) + + +class UserIdForm(forms.Form): + user = forms.IntegerField( + label="User ID", + required=False, + ) diff --git a/registrasion/reporting/reports.py b/registrasion/reporting/reports.py index f8339a5d..e756622e 100644 --- a/registrasion/reporting/reports.py +++ b/registrasion/reporting/reports.py @@ -13,6 +13,7 @@ _all_report_views = [] class Report(object): def __init__(self, title, headings, data, link_view=None): + self._title = title self._headings = headings self._data = data self._link_view = link_view @@ -66,12 +67,15 @@ def report_view(title, form_type=None): else: form = None - report = view(request, form, *a, **k) + reports = view(request, form, *a, **k) + + if isinstance(reports, Report): + reports = [reports] ctx = { "title": title, "form": form, - "report": report, + "reports": reports, } return render(request, "registrasion/report.html", ctx) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index d44de514..ec1e03db 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -1,13 +1,16 @@ +import forms + from django.contrib.auth.decorators import user_passes_test from django.core.urlresolvers import reverse from django.db import models from django.db.models import F, Q -from django.db.models import Sum +from django.db.models import Count, Sum from django.db.models import Case, When, Value from django.shortcuts import render -from registrasion import forms +from registrasion.controllers.item import ItemController from registrasion.models import commerce +from registrasion.models import people from registrasion import views from reports import get_all_reports @@ -194,3 +197,110 @@ def credit_notes(request, form): ]) return Report("Credit Notes", headings, data, link_view="credit_note") + + +@report_view("Attendee", form_type=forms.UserIdForm) +def attendee(request, form, attendee_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(): + return attendee_list(request) + + if attendee_id is None: + attendee_id = form.user + + attendee = people.Attendee.objects.get(id=attendee_id) + + reports = [] + + # TODO: METADATA. + + ic = ItemController(attendee.user) + # Paid products + headings = ["Product", "Quantity"] + data = [] + + for pq in ic.items_purchased(): + data.append([ + pq.product, + pq.quantity, + ]) + + reports.append(Report("Paid Products", headings, data)) + + # Unpaid products + headings = ["Product", "Quantity"] + data = [] + + for pq in ic.items_pending(): + data.append([ + pq.product, + pq.quantity, + ]) + + reports.append( Report("Unpaid Products", headings, data)) + + # Invoices + headings = ["Invoice ID", "Status", "Value"] + data = [] + + invoices = commerce.Invoice.objects.filter( + user=attendee.user, + ) + for invoice in invoices: + data.append([ + invoice.id, invoice.get_status_display(), invoice.value, + ]) + + reports.append(Report("Invoices", headings, data, link_view="invoice")) + + # Credit Notes + headings = ["Note ID", "Status", "Value"] + data = [] + + credit_notes = commerce.CreditNote.objects.filter( + invoice__user=attendee.user, + ) + for credit_note in credit_notes: + data.append([ + credit_note.id, credit_note.status, credit_note.value, + ]) + + reports.append( + Report("Credit Notes", headings, data, link_view="credit_note") + ) + + return reports + + +def attendee_list(request): + ''' Returns a list of all attendees. ''' + + attendees = people.Attendee.objects.all().select_related( + "attendeeprofilebase", + ) + + attendees = attendees.values("id", "user__email").annotate( + has_registered=Count( + Q(user__invoice__status=commerce.Invoice.STATUS_PAID) + ), + ) + + headings = [ + "User ID", "Email", "Has registered", + ] + + data = [] + + for attendee in attendees: + data.append([ + attendee["id"], + attendee["user__email"], + attendee["has_registered"], + ]) + + # Sort by whether they've registered, then ID. + data.sort(key=lambda attendee: (-attendee[2], attendee[0])) + + return Report("Attendees", headings, data, link_view="attendee") diff --git a/registrasion/templatetags/registrasion_tags.py b/registrasion/templatetags/registrasion_tags.py index 9074781c..2d6f51a4 100644 --- a/registrasion/templatetags/registrasion_tags.py +++ b/registrasion/templatetags/registrasion_tags.py @@ -1,31 +1,12 @@ from registrasion.models import commerce -from registrasion.models import inventory from registrasion.controllers.category import CategoryController +from registrasion.controllers.item import ItemController -from collections import namedtuple from django import template -from django.db.models import Case -from django.db.models import Q from django.db.models import Sum -from django.db.models import When -from django.db.models import Value register = template.Library() -_ProductAndQuantity = namedtuple("ProductAndQuantity", ["product", "quantity"]) - - -class ProductAndQuantity(_ProductAndQuantity): - ''' Class that holds a product and a quantity. - - Attributes: - product (models.inventory.Product) - - quantity (int) - - ''' - pass - @register.assignment_tag(takes_context=True) def available_categories(context): @@ -67,71 +48,18 @@ def invoices(context): @register.assignment_tag(takes_context=True) def items_pending(context): - ''' Gets all of the items that the user has reserved, but has not yet - paid for. - - Returns: - [ProductAndQuantity, ...]: A list of product-quantity pairs for the - items that the user has not yet paid for. - - ''' - - all_items = commerce.ProductItem.objects.filter( - cart__user=context.request.user, - cart__status=commerce.Cart.STATUS_ACTIVE, - ).select_related( - "product", - "product__category", - ).order_by( - "product__category__order", - "product__order", - ) - return all_items + ''' Gets all of the items that the user from this context has reserved.''' + return ItemController(context.request.user).items_pending() @register.assignment_tag(takes_context=True) def items_purchased(context, category=None): - ''' Aggregates the items that this user has purchased. + ''' Returns the items purchased for this user. ''' - 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. - - ''' - - in_cart = ( - Q(productitem__cart__user=context.request.user) & - Q(productitem__cart__status=commerce.Cart.STATUS_PAID) + return ItemController(context.request.user).items_purchased( + category=category ) - quantities_in_cart = When( - in_cart, - then="productitem__quantity", - ) - - quantities_or_zero = Case( - quantities_in_cart, - default=Value(0), - ) - - products = inventory.Product.objects - - if category: - products = products.filter(category=category) - - products = products.select_related("category") - products = products.annotate(quantity=Sum(quantities_or_zero)) - products = products.filter(quantity__gt=0) - - out = [] - for prod in products: - out.append(ProductAndQuantity(prod, prod.quantity)) - return out - @register.filter def multiply(value, arg): diff --git a/registrasion/urls.py b/registrasion/urls.py index b6b120c1..5c304091 100644 --- a/registrasion/urls.py +++ b/registrasion/urls.py @@ -37,6 +37,8 @@ public = [ reports = [ url(r"^$", reporting_views.reports_list, name="reports_list"), + url(r"^attendee/?$", reporting_views.attendee, name="attendee"), + url(r"^attendee/([0-9]*)$", reporting_views.attendee, name="attendee"), url( r"^credit_notes/?$", reporting_views.credit_notes, diff --git a/registrasion/views.py b/registrasion/views.py index 7216273e..95e97793 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -747,6 +747,7 @@ def credit_note(request, note_id, access_code=None): ''' + note_id = int(note_id) current_note = CreditNoteController.for_id_or_404(note_id) apply_form = forms.ApplyCreditNoteForm(