symposion_app/registrasion/reporting/views.py

857 lines
25 KiB
Python
Raw Normal View History

2017-04-17 12:55:48 +00:00
from registrasion.reporting import forms
2016-09-19 05:03:21 +00:00
import collections
import datetime
import itertools
2016-09-19 05:03:21 +00:00
from django.conf import settings
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import User
2016-09-01 23:55:29 +00:00
from django.core.urlresolvers import reverse
2016-08-26 04:59:54 +00:00
from django.db import models
from django.db.models import F, Q
2016-09-19 05:03:21 +00:00
from django.db.models import Count, Max, Sum
2016-08-26 04:59:54 +00:00
from django.db.models import Case, When, Value
2016-10-05 18:59:08 +00:00
from django.db.models.fields.related import RelatedField
from django.shortcuts import render
from registrasion.controllers.cart import CartController
from registrasion.controllers.item import ItemController
2016-09-02 01:20:03 +00:00
from registrasion.models import commerce
from registrasion.models import people
from registrasion import util
2016-09-02 01:20:03 +00:00
from registrasion import views
from symposion.schedule import models as schedule_models
2017-04-17 12:55:48 +00:00
from registrasion.reporting.reports import get_all_reports
from registrasion.reporting.reports import Links
from registrasion.reporting.reports import ListReport
from registrasion.reporting.reports import QuerysetReport
from registrasion.reporting.reports import report_view
2016-08-25 11:10:07 +00:00
def CURRENCY():
return models.DecimalField(decimal_places=2)
AttendeeProfile = util.get_object_from_name(settings.ATTENDEE_PROFILE_MODEL)
2016-09-01 23:55:29 +00:00
@user_passes_test(views._staff_only)
def reports_list(request):
''' Lists all of the reports currently available. '''
reports = []
2016-09-02 00:10:21 +00:00
for report in get_all_reports():
2016-09-01 23:55:29 +00:00
reports.append({
2016-09-02 01:25:50 +00:00
"name": report.__name__,
"url": reverse(report),
"description": report.__doc__,
2016-09-01 23:55:29 +00:00
})
reports.sort(key=lambda report: report["name"])
ctx = {
2016-09-02 01:25:50 +00:00
"reports": reports,
2016-09-01 23:55:29 +00:00
}
return render(request, "registrasion/reports_list.html", ctx)
# Report functions
@report_view("Reconcilitation")
def reconciliation(request, form):
''' Shows the summary of sales, and the full history of payments and
refunds into the system. '''
return [
sales_payment_summary(),
items_sold(),
payments(),
credit_note_refunds(),
]
2016-09-01 23:55:29 +00:00
def items_sold():
''' Summarises the items sold and discounts granted for a given set of
products, or products from categories. '''
data = None
headings = None
line_items = commerce.LineItem.objects.filter(
invoice__status=commerce.Invoice.STATUS_PAID,
).select_related("invoice")
2016-08-25 11:05:02 +00:00
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"),
)
2016-08-25 11:05:02 +00:00
2017-04-17 12:55:48 +00:00
print(line_items)
headings = ["Description", "Quantity", "Price", "Total"]
2016-08-26 03:40:49 +00:00
data = []
total_income = 0
for line in line_items:
cost = line["total_quantity"] * line["price"]
2016-08-26 03:40:49 +00:00
data.append([
line["description"], line["total_quantity"],
line["price"], cost,
2016-08-26 03:40:49 +00:00
])
total_income += cost
data.append([
"(TOTAL)", "--", "--", total_income,
])
2016-08-26 04:59:54 +00:00
return ListReport("Items sold", headings, data)
2016-08-26 04:59:54 +00:00
def sales_payment_summary():
''' Summarises paid items and payments. '''
def value_or_zero(aggregate, key):
return aggregate[key] or 0
def sum_amount(payment_set):
a = payment_set.values("amount").aggregate(total=Sum("amount"))
return value_or_zero(a, "total")
headings = ["Category", "Total"]
data = []
# Summarise all sales made (= income.)
sales = commerce.LineItem.objects.filter(
invoice__status=commerce.Invoice.STATUS_PAID,
).values(
"price", "quantity"
).aggregate(
total=Sum(F("price") * F("quantity"), output_field=CURRENCY()),
)
sales = value_or_zero(sales, "total")
all_payments = sum_amount(commerce.PaymentBase.objects.all())
# Manual payments
# Credit notes generated (total)
# Payments made by credit note
# Claimed credit notes
all_credit_notes = 0 - sum_amount(commerce.CreditNote.objects.all())
unclaimed_credit_notes = 0 - sum_amount(commerce.CreditNote.unclaimed())
claimed_credit_notes = sum_amount(
commerce.CreditNoteApplication.objects.all()
)
refunded_credit_notes = 0 - sum_amount(commerce.CreditNote.refunded())
data.append(["Items on paid invoices", sales])
data.append(["All payments", all_payments])
data.append(["Sales - Payments ", sales - all_payments])
data.append(["All credit notes", all_credit_notes])
data.append(["Credit notes paid on invoices", claimed_credit_notes])
data.append(["Credit notes refunded", refunded_credit_notes])
data.append(["Unclaimed credit notes", unclaimed_credit_notes])
data.append([
"Credit notes - (claimed credit notes + unclaimed credit notes)",
all_credit_notes - claimed_credit_notes -
refunded_credit_notes - unclaimed_credit_notes,
])
return ListReport("Sales and Payments Summary", headings, data)
def payments():
''' Shows the history of payments into the system '''
payments = commerce.PaymentBase.objects.all()
return QuerysetReport(
"Payments",
["invoice__id", "id", "reference", "amount"],
payments,
link_view=views.invoice,
)
def credit_note_refunds():
''' Shows all of the credit notes that have been generated. '''
notes_refunded = commerce.CreditNote.refunded()
return QuerysetReport(
"Credit note refunds",
["id", "creditnoterefund__reference", "amount"],
notes_refunded,
link_view=views.credit_note,
)
def group_by_cart_status(queryset, order, values):
queryset = queryset.annotate(
is_reserved=Case(
2016-09-05 11:10:21 +00:00
When(cart__in=commerce.Cart.reserved_carts(), then=Value(True)),
default=Value(False),
output_field=models.BooleanField(),
),
)
values = queryset.order_by(*order).values(*values)
values = values.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),
)),
)
return values
@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 = group_by_cart_status(
items,
["product__category__order", "product__order"],
["product", "product__category__name", "product__name"],
)
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"],
])
2016-08-26 04:59:54 +00:00
return ListReport("Inventory", headings, data)
2016-09-02 00:40:28 +00:00
@report_view("Product status", form_type=forms.DiscountForm)
def discount_status(request, form):
''' Summarises the usage of a given discount. '''
discounts = form.cleaned_data["discount"]
items = commerce.DiscountItem.objects.filter(
Q(discount__in=discounts),
).select_related("cart", "product", "product__category")
items = group_by_cart_status(
items,
["discount",],
["discount", "discount__description",],
)
headings = [
"Discount", "Paid", "Reserved", "Unreserved", "Refunded",
]
data = []
for item in items:
data.append([
item["discount__description"],
item["total_paid"],
item["total_reserved"],
item["total_unreserved"],
item["total_refunded"],
])
return ListReport("Usage by item", headings, data)
2016-09-19 05:03:21 +00:00
@report_view("Paid invoices by date", form_type=forms.ProductAndCategoryForm)
def paid_invoices_by_date(request, form):
''' Shows the number of paid invoices containing given products or
categories per day. '''
products = form.cleaned_data["product"]
categories = form.cleaned_data["category"]
invoices = commerce.Invoice.objects.filter(
(
Q(lineitem__product__in=products) |
Q(lineitem__product__category__in=categories)
),
2016-09-19 05:03:21 +00:00
status=commerce.Invoice.STATUS_PAID,
)
# Invoices with payments will be paid at the time of their latest payment
2016-09-19 05:03:21 +00:00
payments = commerce.PaymentBase.objects.all()
payments = payments.filter(
invoice__in=invoices,
)
payments = payments.order_by("invoice")
invoice_max_time = payments.values("invoice").annotate(
max_time=Max("time")
)
2016-09-19 05:03:21 +00:00
# Zero-value invoices will have no payments, so they're paid at issue time
zero_value_invoices = invoices.filter(value=0)
2016-09-19 05:03:21 +00:00
times = itertools.chain(
(line["max_time"] for line in invoice_max_time),
(invoice.issue_time for invoice in zero_value_invoices),
)
by_date = collections.defaultdict(int)
for time in times:
2016-09-19 05:03:21 +00:00
date = datetime.datetime(
year=time.year, month=time.month, day=time.day
)
by_date[date] += 1
data = [(date, count) for date, count in sorted(by_date.items())]
data = [(date.strftime("%Y-%m-%d"), count) for date, count in data]
return ListReport(
"Paid Invoices By Date",
["date", "count"],
data,
)
2016-09-02 00:40:28 +00:00
@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",
)
return QuerysetReport(
"Credit Notes",
["id", "invoice__user__attendee__attendeeprofilebase__invoice_recipient", "status", "value"], # NOQA
notes,
headings=["id", "Owner", "Status", "Value"],
link_view=views.credit_note,
)
2016-10-13 16:32:30 +00:00
@report_view("Invoices")
def invoices(request,form):
''' Shows all of the invoices in the system. '''
2017-01-15 01:16:03 +00:00
invoices = commerce.Invoice.objects.all().order_by("status", "id")
2016-10-13 16:32:30 +00:00
return QuerysetReport(
"Invoices",
["id", "recipient", "value", "get_status_display"],
invoices,
headings=["id", "Recipient", "Value", "Status"],
link_view=views.invoice,
)
class AttendeeListReport(ListReport):
def get_link(self, argument):
return reverse(self._link_view) + "?user=%d" % int(argument)
@report_view("Attendee", form_type=forms.UserIdForm)
def attendee(request, form, user_id=None):
''' Returns a list of all manifested attendees if no attendee is specified,
else displays the attendee manifest. '''
if user_id is None and form.cleaned_data["user"] is not None:
user_id = form.cleaned_data["user"]
2016-10-14 18:19:10 +00:00
if user_id is None:
return attendee_list(request)
2017-04-17 12:55:48 +00:00
print(user_id)
attendee = people.Attendee.objects.get(user__id=user_id)
name = attendee.attendeeprofilebase.attendee_name()
reports = []
profile_data = []
try:
profile = people.AttendeeProfileBase.objects.get_subclass(
attendee=attendee
)
fields = profile._meta.get_fields()
except people.AttendeeProfileBase.DoesNotExist:
fields = []
exclude = set(["attendeeprofilebase_ptr", "id"])
for field in fields:
if field.name in exclude:
# Not actually important
continue
if not hasattr(field, "verbose_name"):
continue # Not a publicly visible field
value = getattr(profile, field.name)
if isinstance(field, models.ManyToManyField):
value = ", ".join(str(i) for i in value.all())
profile_data.append((field.verbose_name, value))
cart = CartController.for_user(attendee.user)
reservation = cart.cart.reservation_duration + cart.cart.time_last_updated
profile_data.append(("Current cart reserved until", reservation))
reports.append(ListReport("Profile", ["", ""], profile_data))
links = []
2017-01-14 20:55:52 +00:00
links.append((
reverse(views.badge, args=[user_id]),
"View badge",
))
links.append((
reverse(views.amend_registration, args=[user_id]),
"Amend current cart",
))
links.append((
reverse(views.extend_reservation, args=[user_id]),
"Extend reservation",
))
reports.append(Links("Actions for " + name, links))
2016-09-13 06:26:40 +00:00
# Paid and pending products
ic = ItemController(attendee.user)
2016-09-13 06:26:40 +00:00
reports.append(ListReport(
"Paid Products",
["Product", "Quantity"],
[(pq.product, pq.quantity) for pq in ic.items_purchased()],
))
reports.append(ListReport(
"Unpaid Products",
["Product", "Quantity"],
[(pq.product, pq.quantity) for pq in ic.items_pending()],
))
# Invoices
invoices = commerce.Invoice.objects.filter(
user=attendee.user,
)
reports.append(QuerysetReport(
"Invoices",
["id", "get_status_display", "value"],
invoices,
2016-09-13 08:47:51 +00:00
headings=["Invoice ID", "Status", "Value"],
link_view=views.invoice,
))
# Credit Notes
credit_notes = commerce.CreditNote.objects.filter(
invoice__user=attendee.user,
).select_related("invoice", "creditnoteapplication", "creditnoterefund")
reports.append(QuerysetReport(
"Credit Notes",
["id", "status", "value"],
credit_notes,
link_view=views.credit_note,
))
# All payments
payments = commerce.PaymentBase.objects.filter(
invoice__user=attendee.user,
).select_related("invoice")
2016-10-05 19:52:56 +00:00
reports.append(QuerysetReport(
"Payments",
["invoice__id", "id", "reference", "amount"],
payments,
link_view=views.invoice,
))
return reports
def attendee_list(request):
''' Returns a list of all attendees. '''
2016-10-05 19:52:56 +00:00
attendees = people.Attendee.objects.select_related(
"attendeeprofilebase",
"user",
)
2016-09-02 05:37:57 +00:00
2016-10-05 19:52:56 +00:00
profiles = AttendeeProfile.objects.filter(
attendee__in=attendees
).select_related(
"attendee", "attendee__user",
)
profiles_by_attendee = dict((i.attendee, i) for i in profiles)
attendees = attendees.annotate(
has_registered=Count(
Q(user__invoice__status=commerce.Invoice.STATUS_PAID)
),
)
headings = [
"User ID", "Name", "Email", "Has registered",
]
data = []
for a in attendees:
data.append([
a.user.id,
2016-10-05 19:52:56 +00:00
(profiles_by_attendee[a].attendee_name()
if a in profiles_by_attendee else ""),
a.user.email,
a.has_registered > 0,
])
# Sort by whether they've registered, then ID.
data.sort(key=lambda a: (-a[3], a[0]))
return AttendeeListReport("Attendees", headings, data, link_view=attendee)
ProfileForm = forms.model_fields_form_factory(AttendeeProfile)
@report_view(
"Attendees By Product/Category",
form_type=forms.mix_form(
forms.ProductAndCategoryForm, ProfileForm, forms.GroupByForm
),
)
def attendee_data(request, form, user_id=None):
''' Lists attendees for a given product/category selection along with
profile data.'''
status_display = {
commerce.Cart.STATUS_ACTIVE: "Unpaid",
commerce.Cart.STATUS_PAID: "Paid",
commerce.Cart.STATUS_RELEASED: "Refunded",
}
output = []
by_category = form.cleaned_data["group_by"] == forms.GroupByForm.GROUP_BY_CATEGORY
products = form.cleaned_data["product"]
categories = form.cleaned_data["category"]
fields = form.cleaned_data["fields"]
name_field = AttendeeProfile.name_field()
items = commerce.ProductItem.objects.filter(
Q(product__in=products) | Q(product__category__in=categories),
).exclude(
cart__status=commerce.Cart.STATUS_RELEASED
).select_related(
"cart", "cart__user", "product", "product__category",
).order_by("cart__status")
2016-12-07 06:38:37 +00:00
# Add invoice nag link
links = []
invoice_mailout = reverse(views.invoice_mailout, args=[]) + "?" + request.META["QUERY_STRING"]
links += [
(invoice_mailout + "&status=1", "Send invoice reminders",),
(invoice_mailout + "&status=2", "Send mail for paid invoices",),
]
2016-12-07 06:38:37 +00:00
if items.count() > 0:
output.append(Links("Actions", links))
# Make sure we select all of the related fields
related_fields = set(
field for field in fields
if isinstance(AttendeeProfile._meta.get_field(field), RelatedField)
)
# Get all of the relevant attendee profiles in one hit.
profiles = AttendeeProfile.objects.filter(
attendee__user__cart__productitem__in=items
).select_related("attendee__user").prefetch_related(*related_fields)
by_user = {}
for profile in profiles:
by_user[profile.attendee.user] = profile
cart = "attendee__user__cart"
cart_status = cart + "__status"
product = cart + "__productitem__product"
product_name = product + "__name"
category = product + "__category"
category_name = category + "__name"
if by_category:
grouping_fields = (category, category_name)
order_by = (category, )
first_column = "Category"
group_name = lambda i: "%s" % (i[category_name], )
else:
grouping_fields = (product, product_name, category_name)
order_by = (category, )
first_column = "Product"
group_name = lambda i: "%s - %s" % (i[category_name], i[product_name])
2016-09-20 04:04:38 +00:00
# Group the responses per-field.
for field in fields:
2016-10-05 18:59:08 +00:00
concrete_field = AttendeeProfile._meta.get_field(field)
field_verbose = concrete_field.verbose_name
# Render the correct values for related fields
if field in related_fields:
2016-10-05 18:59:08 +00:00
# Get all of the IDs that will appear
all_ids = profiles.order_by(field).values(field)
all_ids = [i[field] for i in all_ids if i[field] is not None]
# Get all of the concrete objects for those IDs
model = concrete_field.related_model
all_objects = model.objects.filter(id__in=all_ids)
all_objects_by_id = dict((i.id, i) for i in all_objects)
# Define a function to render those IDs.
def display_field(value):
if value in all_objects_by_id:
return all_objects_by_id[value]
else:
return None
else:
def display_field(value):
return value
2016-09-20 04:04:38 +00:00
status_count = lambda status: Case(When(
attendee__user__cart__status=status,
then=Value(1),
),
default=Value(0),
output_field=models.fields.IntegerField(),
)
paid_count = status_count(commerce.Cart.STATUS_PAID)
unpaid_count = status_count(commerce.Cart.STATUS_ACTIVE)
groups = profiles.order_by(
*(order_by + (field, ))
).values(
*(grouping_fields + (field, ))
2016-09-20 04:04:38 +00:00
).annotate(
paid_count=Sum(paid_count),
unpaid_count=Sum(unpaid_count),
)
output.append(ListReport(
"Grouped by %s" % field_verbose,
[first_column, field_verbose, "paid", "unpaid"],
[
(
group_name(group),
2016-10-05 18:59:08 +00:00
display_field(group[field]),
group["paid_count"] or 0,
group["unpaid_count"] or 0,
)
for group in groups
],
))
# DO the report for individual attendees
field_names = [
AttendeeProfile._meta.get_field(field).verbose_name for field in fields
]
2016-10-05 18:59:08 +00:00
def display_field(profile, field):
field_type = AttendeeProfile._meta.get_field(field)
attr = getattr(profile, field)
if isinstance(field_type, models.ManyToManyField):
return [str(i) for i in attr.all()] or ""
2016-10-05 18:59:08 +00:00
else:
return attr
2016-09-20 08:44:23 +00:00
headings = ["User ID", "Name", "Email", "Product", "Item Status"] + field_names
data = []
for item in items:
profile = by_user[item.cart.user]
line = [
item.cart.user.id,
getattr(profile, name_field),
2016-09-20 08:44:23 +00:00
profile.attendee.user.email,
item.product,
status_display[item.cart.status],
] + [
2016-10-05 18:59:08 +00:00
display_field(profile, field) for field in fields
]
data.append(line)
output.append(AttendeeListReport(
"Attendees by item with profile data", headings, data, link_view=attendee
))
return output
@report_view(
"Speaker Registration Status",
form_type=forms.ProposalKindForm,
)
def speaker_registrations(request, form):
''' Shows registration status for speakers with a given proposal kind. '''
kinds = form.cleaned_data["kind"]
presentations = schedule_models.Presentation.objects.filter(
2016-10-05 20:07:44 +00:00
proposal_base__kind__in=kinds,
).exclude(
cancelled=True,
)
users = User.objects.filter(
Q(speaker_profile__presentations__in=presentations) |
Q(speaker_profile__copresentations__in=presentations)
)
paid_carts = commerce.Cart.objects.filter(status=commerce.Cart.STATUS_PAID)
2016-10-05 20:07:44 +00:00
paid_carts = Case(
When(cart__in=paid_carts, then=Value(1)),
default=Value(0),
output_field=models.IntegerField(),
)
users = users.annotate(paid_carts=Sum(paid_carts))
2016-10-05 20:07:44 +00:00
users = users.order_by("paid_carts")
return QuerysetReport(
"Speaker Registration Status",
["id", "speaker_profile__name", "email", "paid_carts",],
users,
link_view=attendee,
)
return []
2017-01-14 20:48:43 +00:00
@report_view(
"Manifest",
forms.ProductAndCategoryForm,
)
def manifest(request, form):
''' Produces the registration manifest for people with the given product type.'''
products = form.cleaned_data["product"]
categories = form.cleaned_data["category"]
line_items = (
Q(lineitem__product__in=products) |
Q(lineitem__product__category__in=categories)
)
invoices = commerce.Invoice.objects.filter(
line_items,
status=commerce.Invoice.STATUS_PAID,
).select_related(
"cart",
"user",
"user__attendee",
"user__attendee__attendeeprofilebase"
)
users = set(i.user for i in invoices)
carts = commerce.Cart.objects.filter(
user__in=users
)
items = commerce.ProductItem.objects.filter(
cart__in=carts
).select_related(
"product",
"product__category",
"cart",
"cart__user",
"cart__user__attendee",
"cart__user__attendee__attendeeprofilebase"
).order_by("product__category__order", "product__order")
users = {}
for item in items:
cart = item.cart
if cart.user not in users:
users[cart.user] = {"unpaid": [], "paid": [], "refunded": []}
items = users[cart.user]
if cart.status == commerce.Cart.STATUS_ACTIVE:
items["unpaid"].append(item)
elif cart.status == commerce.Cart.STATUS_PAID:
items["paid"].append(item)
elif cart.status == commerce.Cart.STATUS_RELEASED:
items["refunded"].append(item)
users_by_name = list(users.keys())
users_by_name.sort(key=(
lambda i: i.attendee.attendeeprofilebase.attendee_name().lower()
))
headings = ["User ID", "Name", "Paid", "Unpaid", "Refunded"]
def format_items(item_list):
strings = [
"%d x %s" % (item.quantity, str(item.product)) for item in item_list
]
return ", \n".join(strings)
output = []
for user in users_by_name:
items = users[user]
output.append([
user.id,
user.attendee.attendeeprofilebase.attendee_name(),
format_items(items["paid"]),
format_items(items["unpaid"]),
format_items(items["refunded"]),
])
return ListReport("Manifest", headings, output)
#attendeeprofilebase.attendee_name()