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",
|
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,
|
||||||
|
)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
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
|
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.
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in a new issue