Merge branch 'chrisjrn/reports'

Fixes #46
This commit is contained in:
Christopher Neugebauer 2016-09-02 11:32:28 +10:00
commit 4b6b221086
6 changed files with 351 additions and 20 deletions

View file

@ -345,3 +345,16 @@ class VoucherForm(forms.Form):
help_text="If you have a voucher code, enter it here", help_text="If you have a voucher code, enter it here",
required=False, 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,
)

View file

@ -46,7 +46,7 @@ class Category(models.Model):
from this Category that each attendee may claim. This extends from this Category that each attendee may claim. This extends
across multiple Invoices. 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 available. By convention, your Category for ticket types should
have the lowest display order. have the lowest display order.
''' '''
@ -129,7 +129,7 @@ class Product(models.Model):
pay for it. This reservation duration determines how long an item pay for it. This reservation duration determines how long an item
should be allowed to be reserved whilst being unpaid. 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. within each Category.
''' '''

View file

View 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)

View 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")

View file

@ -1,22 +1,54 @@
import views 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( from .views import (
"registrasion.views", product_category,
url(r"^category/([0-9]+)$", "product_category", name="product_category"), checkout,
url(r"^checkout$", "checkout", name="checkout"), credit_note,
url(r"^credit_note/([0-9]+)$", views.credit_note, name="credit_note"), invoice,
url(r"^invoice/([0-9]+)$", "invoice", name="invoice"), manual_payment,
url(r"^invoice/([0-9]+)/([A-Z0-9]+)$", views.invoice, name="invoice"), refund,
url(r"^invoice/([0-9]+)/manual_payment$", invoice_access,
views.manual_payment, name="manual_payment"), edit_profile,
url(r"^invoice/([0-9]+)/refund$", guided_registration,
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"),
) )
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.
]