From 25608b1653e7a113a239d11ffe9d96e6ff2c320e Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 2 Sep 2016 14:33:23 +1000 Subject: [PATCH 1/8] Moves reports forms into reporting sub package --- registrasion/forms.py | 13 ------------- registrasion/reporting/forms.py | 15 +++++++++++++++ registrasion/reporting/views.py | 3 ++- 3 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 registrasion/reporting/forms.py 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..b741e51b --- /dev/null +++ b/registrasion/reporting/forms.py @@ -0,0 +1,15 @@ +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, + ) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index d44de514..d6b6261c 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -1,3 +1,5 @@ +import forms + from django.contrib.auth.decorators import user_passes_test from django.core.urlresolvers import reverse from django.db import models @@ -6,7 +8,6 @@ from django.db.models import Sum from django.db.models import Case, When, Value from django.shortcuts import render -from registrasion import forms from registrasion.models import commerce from registrasion import views From 48a036204d372c117e68f1f34630d95343d384b9 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 2 Sep 2016 15:14:54 +1000 Subject: [PATCH 2/8] Reporting framework can now display multiple sections. --- registrasion/reporting/reports.py | 8 ++++++-- registrasion/urls.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) 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/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, From e27e322c41569da61b58cfc9cd0424268f625ad2 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 2 Sep 2016 15:15:15 +1000 Subject: [PATCH 3/8] Adds the attendee list and stubs the attendee manifest reports --- registrasion/reporting/forms.py | 7 ++++ registrasion/reporting/views.py | 70 ++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/registrasion/reporting/forms.py b/registrasion/reporting/forms.py index b741e51b..f4902660 100644 --- a/registrasion/reporting/forms.py +++ b/registrasion/reporting/forms.py @@ -13,3 +13,10 @@ class ProductAndCategoryForm(forms.Form): queryset=inventory.Category.objects.all(), required=False, ) + + +class UserIdForm(forms.Form): + user = forms.IntegerField( + label="User ID", + required=False, + ) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index d6b6261c..fcb5d1e6 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -4,11 +4,12 @@ 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.models import commerce +from registrasion.models import people from registrasion import views from reports import get_all_reports @@ -195,3 +196,70 @@ 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) + + reports = [] + + # TODO: METADATA. + + + # Paid products + headings = ["Product", "Quantity"] + data = [] + reports.append(Report("Paid Products", headings, data)) + + # Unpaid products + headings = ["Product", "Quantity"] + data = [] + reports.append( Report("Unpaid Products", headings, data)) + + # Invoices + headings = ["Invoice ID", "Status", "Amount"] + data = [] + reports.append( Report("Invoices", headings, data)) + + # Credit Notes + headings = ["Note ID", "Status", "Value"] + data = [] + reports.append( Report("Credit Notes", headings, data)) + + return reports + + +def attendee_list(request): + ''' Returns a list of all attendees. ''' + + attendees = people.Attendee.objects.all().select_related( + "attendeeprofilebase", + ) + attendees = attendees.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.user.id, + attendee.user.email, + attendee.has_registered > 0, + ]) + + # 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") From d58b2811f9244d3ea9cd7b20fd8286a2b960ffa8 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 2 Sep 2016 15:37:57 +1000 Subject: [PATCH 4/8] Makes the attendee list work better. --- registrasion/reporting/views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index fcb5d1e6..58c86430 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -240,7 +240,8 @@ def attendee_list(request): attendees = people.Attendee.objects.all().select_related( "attendeeprofilebase", ) - attendees = attendees.annotate( + + attendees = attendees.values("id", "user__email").annotate( has_registered=Count( Q(user__invoice__status=commerce.Invoice.STATUS_PAID) ), @@ -254,9 +255,9 @@ def attendee_list(request): for attendee in attendees: data.append([ - attendee.user.id, - attendee.user.email, - attendee.has_registered > 0, + attendee["id"], + attendee["user__email"], + attendee["has_registered"], ]) # Sort by whether they've registered, then ID. From 17fc874212bd861749fa660043863fb9dee980f2 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 2 Sep 2016 15:43:01 +1000 Subject: [PATCH 5/8] Attendee manifest now displays credit notes. --- registrasion/reporting/views.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index 58c86430..f52c2d79 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -206,6 +206,11 @@ def attendee(request, form, attendee_id=None): 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. @@ -222,14 +227,34 @@ def attendee(request, form, attendee_id=None): reports.append( Report("Unpaid Products", headings, data)) # Invoices - headings = ["Invoice ID", "Status", "Amount"] + headings = ["Invoice ID", "Status", "Value"] data = [] - reports.append( Report("Invoices", headings, 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 = [] - reports.append( Report("Credit Notes", headings, 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 From 68aa9b067bdb2d25c2ed50fa19ff95e816312724 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 2 Sep 2016 15:57:20 +1000 Subject: [PATCH 6/8] Factors items_pending and items_purchased into ItemController --- registrasion/controllers/item.py | 95 +++++++++++++++++++ .../templatetags/registrasion_tags.py | 84 ++-------------- 2 files changed, 101 insertions(+), 78 deletions(-) create mode 100644 registrasion/controllers/item.py 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/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): From 964fe380da0974089fbb21602a6d3f2ffbe8edc4 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 2 Sep 2016 15:57:44 +1000 Subject: [PATCH 7/8] Attendee manifest page now reports the items a user has pending and purchased. --- registrasion/reporting/views.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index f52c2d79..ec1e03db 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -8,6 +8,7 @@ from django.db.models import Count, Sum from django.db.models import Case, When, Value from django.shortcuts import render +from registrasion.controllers.item import ItemController from registrasion.models import commerce from registrasion.models import people from registrasion import views @@ -215,15 +216,29 @@ def attendee(request, form, attendee_id=None): # 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 From 5b03ae8ff6296e5c982118bcd1951fdd6992e8b3 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 2 Sep 2016 16:03:29 +1000 Subject: [PATCH 8/8] Fixes credit note bug --- registrasion/views.py | 1 + 1 file changed, 1 insertion(+) 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(