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",
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
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.
'''

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
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.
]