commit
						4b6b221086
					
				
					 6 changed files with 351 additions and 20 deletions
				
			
		|  | @ -345,3 +345,16 @@ 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, | ||||
|     ) | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ class Category(models.Model): | |||
|             from this Category that each attendee may claim. This extends | ||||
|             across multiple Invoices. | ||||
| 
 | ||||
|         display_order (int): An ascending order for displaying the Categories | ||||
|         order (int): An ascending order for displaying the Categories | ||||
|             available. By convention, your Category for ticket types should | ||||
|             have the lowest display order. | ||||
|     ''' | ||||
|  | @ -129,7 +129,7 @@ class Product(models.Model): | |||
|             pay for it. This reservation duration determines how long an item | ||||
|             should be allowed to be reserved whilst being unpaid. | ||||
| 
 | ||||
|         display_order (int): An ascending order for displaying the Products | ||||
|         order (int): An ascending order for displaying the Products | ||||
|             within each Category. | ||||
| 
 | ||||
|     ''' | ||||
|  |  | |||
							
								
								
									
										0
									
								
								registrasion/reporting/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								registrasion/reporting/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										90
									
								
								registrasion/reporting/reports.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								registrasion/reporting/reports.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | |||
| from django.contrib.auth.decorators import user_passes_test | ||||
| from django.shortcuts import render | ||||
| from functools import wraps | ||||
| 
 | ||||
| from registrasion import views | ||||
| 
 | ||||
| 
 | ||||
| ''' A list of report views objects that can be used to load a list of | ||||
| reports. ''' | ||||
| _all_report_views = [] | ||||
| 
 | ||||
| 
 | ||||
| class Report(object): | ||||
| 
 | ||||
|     def __init__(self, title, headings, data, link_view=None): | ||||
|         self._headings = headings | ||||
|         self._data = data | ||||
|         self._link_view = link_view | ||||
| 
 | ||||
|     @property | ||||
|     def title(self): | ||||
|         ''' Returns the title for this report. ''' | ||||
|         return self._title | ||||
| 
 | ||||
|     @property | ||||
|     def headings(self): | ||||
|         ''' Returns the headings for the table. ''' | ||||
|         return self._headings | ||||
| 
 | ||||
|     @property | ||||
|     def data(self): | ||||
|         ''' Returns the data rows for the table. ''' | ||||
|         return self._data | ||||
| 
 | ||||
|     @property | ||||
|     def link_view(self): | ||||
|         ''' Returns the URL name or the view callable that can be used to | ||||
|         view the row's detail. The left-most value is passed into `reverse` | ||||
|         as an argument. ''' | ||||
| 
 | ||||
|         return self._link_view | ||||
| 
 | ||||
| 
 | ||||
| def report_view(title, form_type=None): | ||||
|     ''' Decorator that converts a report view function into something that | ||||
|     displays a Report. | ||||
| 
 | ||||
|     Arguments: | ||||
|         title (str): | ||||
|             The title of the report. | ||||
|         form_type (Optional[forms.Form]): | ||||
|             A form class that can make this report display things. If not | ||||
|             supplied, no form will be displayed. | ||||
| 
 | ||||
|     ''' | ||||
| 
 | ||||
|     def _report(view): | ||||
| 
 | ||||
|         @wraps(view) | ||||
|         @user_passes_test(views._staff_only) | ||||
|         def inner_view(request, *a, **k): | ||||
| 
 | ||||
|             if form_type is not None: | ||||
|                 form = form_type(request.GET) | ||||
|                 form.is_valid() | ||||
|             else: | ||||
|                 form = None | ||||
| 
 | ||||
|             report = view(request, form, *a, **k) | ||||
| 
 | ||||
|             ctx = { | ||||
|                 "title": title, | ||||
|                 "form": form, | ||||
|                 "report": report, | ||||
|             } | ||||
| 
 | ||||
|             return render(request, "registrasion/report.html", ctx) | ||||
| 
 | ||||
|         # Add this report to the list of reports. | ||||
|         _all_report_views.append(inner_view) | ||||
| 
 | ||||
|         # Return the callable | ||||
|         return inner_view | ||||
|     return _report | ||||
| 
 | ||||
| 
 | ||||
| def get_all_reports(): | ||||
|     ''' Returns all the views that have been registered with @report ''' | ||||
| 
 | ||||
|     return list(_all_report_views) | ||||
							
								
								
									
										196
									
								
								registrasion/reporting/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								registrasion/reporting/views.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,196 @@ | |||
| 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 Case, When, Value | ||||
| from django.shortcuts import render | ||||
| 
 | ||||
| from registrasion import forms | ||||
| from registrasion.models import commerce | ||||
| from registrasion import views | ||||
| 
 | ||||
| from reports import get_all_reports | ||||
| from reports import Report | ||||
| from reports import report_view | ||||
| 
 | ||||
| 
 | ||||
| @user_passes_test(views._staff_only) | ||||
| def reports_list(request): | ||||
|     ''' Lists all of the reports currently available. ''' | ||||
| 
 | ||||
|     reports = [] | ||||
| 
 | ||||
|     for report in get_all_reports(): | ||||
|         reports.append({ | ||||
|             "name": report.__name__, | ||||
|             "url": reverse(report), | ||||
|             "description": report.__doc__, | ||||
|         }) | ||||
| 
 | ||||
|     reports.sort(key=lambda report: report["name"]) | ||||
| 
 | ||||
|     ctx = { | ||||
|         "reports": reports, | ||||
|     } | ||||
| 
 | ||||
|     return render(request, "registrasion/reports_list.html", ctx) | ||||
| 
 | ||||
| 
 | ||||
| # Report functions | ||||
| 
 | ||||
| 
 | ||||
| @report_view("Paid items", form_type=forms.ProductAndCategoryForm) | ||||
| def items_sold(request, form): | ||||
|     ''' Summarises the items sold and discounts granted for a given set of | ||||
|     products, or products from categories. ''' | ||||
| 
 | ||||
|     data = None | ||||
|     headings = None | ||||
| 
 | ||||
|     products = form.cleaned_data["product"] | ||||
|     categories = form.cleaned_data["category"] | ||||
| 
 | ||||
|     line_items = commerce.LineItem.objects.filter( | ||||
|         Q(product__in=products) | Q(product__category__in=categories), | ||||
|         invoice__status=commerce.Invoice.STATUS_PAID, | ||||
|     ).select_related("invoice") | ||||
| 
 | ||||
|     line_items = line_items.order_by( | ||||
|         # sqlite requires an order_by for .values() to work | ||||
|         "-price", "description", | ||||
|     ).values( | ||||
|         "price", "description", | ||||
|     ).annotate( | ||||
|         total_quantity=Sum("quantity"), | ||||
|     ) | ||||
| 
 | ||||
|     print line_items | ||||
| 
 | ||||
|     headings = ["Description", "Quantity", "Price", "Total"] | ||||
| 
 | ||||
|     data = [] | ||||
|     total_income = 0 | ||||
|     for line in line_items: | ||||
|         cost = line["total_quantity"] * line["price"] | ||||
|         data.append([ | ||||
|             line["description"], line["total_quantity"], | ||||
|             line["price"], cost, | ||||
|         ]) | ||||
|         total_income += cost | ||||
| 
 | ||||
|     data.append([ | ||||
|         "(TOTAL)", "--", "--", total_income, | ||||
|     ]) | ||||
| 
 | ||||
|     return Report("Paid items", headings, data) | ||||
| 
 | ||||
| 
 | ||||
| @report_view("Product status", form_type=forms.ProductAndCategoryForm) | ||||
| def product_status(request, form): | ||||
|     ''' Summarises the inventory status of the given items, grouping by | ||||
|     invoice status. ''' | ||||
| 
 | ||||
|     products = form.cleaned_data["product"] | ||||
|     categories = form.cleaned_data["category"] | ||||
| 
 | ||||
|     items = commerce.ProductItem.objects.filter( | ||||
|         Q(product__in=products) | Q(product__category__in=categories), | ||||
|     ).select_related("cart", "product") | ||||
| 
 | ||||
|     items = items.annotate( | ||||
|         is_reserved=Case( | ||||
|             When(cart__in=commerce.Cart.reserved_carts(), then=Value(1)), | ||||
|             default=Value(0), | ||||
|             output_field=models.BooleanField(), | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     items = items.order_by( | ||||
|         "product__category__order", | ||||
|         "product__order", | ||||
|     ).values( | ||||
|         "product", | ||||
|         "product__category__name", | ||||
|         "product__name", | ||||
|     ).annotate( | ||||
|         total_paid=Sum(Case( | ||||
|             When( | ||||
|                 cart__status=commerce.Cart.STATUS_PAID, | ||||
|                 then=F("quantity"), | ||||
|             ), | ||||
|             default=Value(0), | ||||
|         )), | ||||
|         total_refunded=Sum(Case( | ||||
|             When( | ||||
|                 cart__status=commerce.Cart.STATUS_RELEASED, | ||||
|                 then=F("quantity"), | ||||
|             ), | ||||
|             default=Value(0), | ||||
|         )), | ||||
|         total_unreserved=Sum(Case( | ||||
|             When( | ||||
|                 ( | ||||
|                     Q(cart__status=commerce.Cart.STATUS_ACTIVE) & | ||||
|                     Q(is_reserved=False) | ||||
|                 ), | ||||
|                 then=F("quantity"), | ||||
|             ), | ||||
|             default=Value(0), | ||||
|         )), | ||||
|         total_reserved=Sum(Case( | ||||
|             When( | ||||
|                 ( | ||||
|                     Q(cart__status=commerce.Cart.STATUS_ACTIVE) & | ||||
|                     Q(is_reserved=True) | ||||
|                 ), | ||||
|                 then=F("quantity"), | ||||
|             ), | ||||
|             default=Value(0), | ||||
|         )), | ||||
|     ) | ||||
| 
 | ||||
|     headings = [ | ||||
|         "Product", "Paid", "Reserved", "Unreserved", "Refunded", | ||||
|     ] | ||||
|     data = [] | ||||
| 
 | ||||
|     for item in items: | ||||
|         data.append([ | ||||
|             "%s - %s" % ( | ||||
|                 item["product__category__name"], item["product__name"] | ||||
|             ), | ||||
|             item["total_paid"], | ||||
|             item["total_reserved"], | ||||
|             item["total_unreserved"], | ||||
|             item["total_refunded"], | ||||
|         ]) | ||||
| 
 | ||||
|     return Report("Inventory", headings, data) | ||||
| 
 | ||||
| 
 | ||||
| @report_view("Credit notes") | ||||
| def credit_notes(request, form): | ||||
|     ''' Shows all of the credit notes in the system. ''' | ||||
| 
 | ||||
|     notes = commerce.CreditNote.objects.all().select_related( | ||||
|         "creditnoterefund", | ||||
|         "creditnoteapplication", | ||||
|         "invoice", | ||||
|         "invoice__user__attendee__attendeeprofilebase", | ||||
|     ) | ||||
| 
 | ||||
|     headings = [ | ||||
|         "id", "Owner", "Status", "Value", | ||||
|     ] | ||||
| 
 | ||||
|     data = [] | ||||
|     for note in notes: | ||||
|         data.append([ | ||||
|             note.id, | ||||
|             note.invoice.user.attendee.attendeeprofilebase.invoice_recipient(), | ||||
|             note.status, | ||||
|             note.value, | ||||
|         ]) | ||||
| 
 | ||||
|     return Report("Credit Notes", headings, data, link_view="credit_note") | ||||
|  | @ -1,22 +1,54 @@ | |||
| import views | ||||
| from reporting import views as reporting_views | ||||
| 
 | ||||
| from django.conf.urls import url, patterns | ||||
| from django.conf.urls import include | ||||
| from django.conf.urls import url | ||||
| 
 | ||||
| urlpatterns = patterns( | ||||
|     "registrasion.views", | ||||
|     url(r"^category/([0-9]+)$", "product_category", name="product_category"), | ||||
|     url(r"^checkout$", "checkout", name="checkout"), | ||||
|     url(r"^credit_note/([0-9]+)$", views.credit_note, name="credit_note"), | ||||
|     url(r"^invoice/([0-9]+)$", "invoice", name="invoice"), | ||||
|     url(r"^invoice/([0-9]+)/([A-Z0-9]+)$", views.invoice, name="invoice"), | ||||
|     url(r"^invoice/([0-9]+)/manual_payment$", | ||||
|         views.manual_payment, name="manual_payment"), | ||||
|     url(r"^invoice/([0-9]+)/refund$", | ||||
|         views.refund, name="refund"), | ||||
|     url(r"^invoice_access/([A-Z0-9]+)$", views.invoice_access, | ||||
|         name="invoice_access"), | ||||
|     url(r"^profile$", "edit_profile", name="attendee_edit"), | ||||
|     url(r"^register$", "guided_registration", name="guided_registration"), | ||||
|     url(r"^register/([0-9]+)$", "guided_registration", | ||||
|         name="guided_registration"), | ||||
| from .views import ( | ||||
|     product_category, | ||||
|     checkout, | ||||
|     credit_note, | ||||
|     invoice, | ||||
|     manual_payment, | ||||
|     refund, | ||||
|     invoice_access, | ||||
|     edit_profile, | ||||
|     guided_registration, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| public = [ | ||||
|     url(r"^category/([0-9]+)$", product_category, name="product_category"), | ||||
|     url(r"^checkout$", 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"), | ||||
|     url(r"^invoice/([0-9]+)/manual_payment$", | ||||
|         manual_payment, name="manual_payment"), | ||||
|     url(r"^invoice/([0-9]+)/refund$", | ||||
|         refund, name="refund"), | ||||
|     url(r"^invoice_access/([A-Z0-9]+)$", invoice_access, | ||||
|         name="invoice_access"), | ||||
|     url(r"^profile$", edit_profile, name="attendee_edit"), | ||||
|     url(r"^register$", guided_registration, name="guided_registration"), | ||||
|     url(r"^register/([0-9]+)$", guided_registration, | ||||
|         name="guided_registration"), | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| reports = [ | ||||
|     url(r"^$", reporting_views.reports_list, name="reports_list"), | ||||
|     url(r"^credit_notes/?$", reporting_views.credit_notes, name="credit_notes"), | ||||
|     url( | ||||
|         r"^product_status/?$", | ||||
|         reporting_views.product_status, | ||||
|         name="product_status", | ||||
|     ), | ||||
|     url(r"^items_sold/?$", reporting_views.items_sold, name="items_sold"), | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     url(r"^reports/", include(reports)), | ||||
|     url(r"^", include(public))  # This one must go last. | ||||
| ] | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer