Changeset - d4b0b52253ae
[Not reviewed]
0 9 0
Joel Addison - 5 years ago 2020-01-04 14:34:17
joel@addison.net.au
Registrasion updates

Show applied vouchers on attendee registration admin page.
Add head title and page title to more areas.
Update the dashboard to show warning for empty categories and have
button to open voucher page.
Fix exception on attendee page when user does not have attendee object.
9 files changed with 79 insertions and 23 deletions:
0 comments (0 inline, 0 general)
pinaxcon/templates/registrasion/_items_list.html
Show inline comments
 
{% if items %}
 
  <ul>
 
    {% for item in items %}
 
      <li>{{ item.quantity }} &times; {{ item.product }} {{ suffix }}</li>
 
    {% endfor %}
 
  </ul>
 
{% else %}
 
  <p>No items.</p>
 
{% endif %}
pinaxcon/templates/registrasion/amend_registration.html
Show inline comments
 
{% extends "registrasion/base.html" %}
 
{% load registrasion_tags %}
 

	
 
{% block content %}
 

	
 
<h2>Item summary for {{ user.attendee.attendeeprofilebase.attendee_name }}
 
  (id={{user.id}})</h2>
 

	
 
<h3>Paid Items</h3>
 

	
 
<p>You cannot remove paid items from someone's registration. You must first
 
  cancel the invoice that added those items. You will need to re-add the items
 
  from that invoice for the user to have them available again.</p>
 

	
 
{% include "registrasion/_items_list.html" with items=paid %}
 

	
 
<br />
 

	
 
<h3>Cancelled Items</h3>
 

	
 
{% include "registrasion/_items_list.html" with items=cancelled %}
 

	
 
<br />
 

	
 
<h3>Amend pending items</h3>
 

	
 
<form class="form-horizontal" method="POST">
 
  {% csrf_token %}
 
  {% include "_form_snippet.html" with form=form %}
 
  <br/>
 
  <input class="btn btn-primary" type="submit">
 
</form>
 

	
 
<br />
 

	
 
<h3>Generate invoice</h3>
 

	
 
<div class="btn-group">
 
  <a class="btn btn-primary" href="{% url "checkout" user.id %}">Check out cart and view invoice</a>
 
</div>
 

	
 
<br />
 

	
 
<h3>Currently applied vouchers</h3>
 

	
 
{% if vouchers %}
 
<ul>
 
  {% for voucher in vouchers %}
 
    <li>{{ voucher.code }}</li>
 
  {% endfor %}
 
</ul>
 
{% else %}
 
<p>No vouchers applied.</p>
 
{% endif %}
 

	
 
<br />
 

	
 
<h3>Apply voucher</h3>
 

	
 
<form class="form-horizontal" method="POST">
 
  {% csrf_token %}
 
  {% include "_form_snippet.html" with form=voucher_form %}
 
  <br/>
 
  <input class="btn btn-primary" type="submit">
 
</form>
 

	
 
{% endblock %}
pinaxcon/templates/registrasion/profile_form.html
Show inline comments
 
{% extends "registrasion/base.html" %}
 
{% load lca2018_tags %}
 

	
 

	
 
{% block head_title %}Your Profile{% endblock %}
 
{% block page_title %}Your Profile{% endblock %}
 

	
 
{% block scripts_extra %}
 
{{ form.media.js }}
 
<script type="text/javascript">
 
    postcode_label = $("label[for='id_profile-state']");
 
    postcode_help = $("#id_profile-state + p");
 
      $('#id_profile-country').change(function () {
 
        if ($(this).val() == 'AU' )  {
 
          postcode_label.addClass('label-required');
 
          postcode_help.show();
 
        } else {
 
          postcode_label.removeClass('label-required');
 
          postcode_help.hide();
 
        } });
 
        $("#id_profile-country").change();
 

	
 
      </script>
 
{% endblock %}
 

	
 
{% block proposals_body %}
 
  <h1>Your Profile</h1>
 
  <p>These details will appear on your badge, your invoices, and will be used to order catered food at the conference.</p>
 

	
 
  <form class="form-horizontal" method="post" action="">
 
    {% csrf_token %}
 
    <fieldset>
 
      {% include "_form_snippet.html" with form=form %}
 
      <br />
 
      <input class="btn btn-primary" type="submit" value="Save Profile" />
 
      {% if user.attendee %}
 
      <a class="btn btn-light" href="{% url "dashboard" %}">Cancel</a>
 
      {% endif %}
 
    </fieldset>
 
  </form>
 
{% endblock %}
pinaxcon/templates/registrasion/review.html
Show inline comments
 
{% extends "registrasion/base.html" %}
 
{% load registrasion_tags %}
 
{% load lca2018_tags %}
 
{% load lca2019_tags %}
 

	
 
{% block page_title %}Review your selection{% endblock %}
 
{% block page_lead %}
 
  Please ensure that you have selected all of the products you require, including
 
  t-shirts and social event tickets.
 
{% endblock %}
 

	
 
{% block scripts_extra %}
 
  {{ voucher_form.media.js }}
 
  {{ form.media.js }}
 
{% endblock %}
 

	
 

	
 
{% block proposals_body %}
 
  <h1 class="mb-4">Order Review</h1>
 
  <h2 class="mb-4">Order Review</h2>
 

	
 
  {% items_pending as pending %}
 
  {% if pending %}
 
  <div class="my-4">
 
    <h2>Current selection</h2>
 
    <h3>Current selection</h3>
 
    <p>You've selected the following items, which will be in your invoice when you check out:<p>
 
    {% include "registrasion/_items_list.html" with items=pending %}
 
  </div>
 
  {% endif %}
 

	
 
  {% items_purchased as purchased %}
 
  {% if purchased %}
 
  <div class="my-4">
 
    <h2>Previously purchased</h2>
 
    <h3>Previously purchased</h3>
 
    <p>You've already paid for the following items:</p>
 
    {% include "registrasion/_items_list.html" with items=purchased suffix="<em>(PAID)</em>" %}
 
  </div>
 
  {% endif %}
 

	
 
  <div class="my-4">
 
    <h2>Modify your selection</h2>
 
    <h3>Modify your selection</h3>
 

	
 
    {% missing_categories as missing %}
 
    {% if missing %}
 
    <div class="alert alert-warning my-4 pb-4">
 
      <h4 class="alert-heading">You have empty categories</h4>
 
      <p>You have <em>not</em> selected anything from the following
 
        categories. If your ticket includes any of these, you still need to
 
        make a selection:
 
      </p>
 

	
 
      {% include "registrasion/_category_list.html" with categories=missing %}
 
    </div>
 
    {% endif %}
 

	
 
    <p>
 
      <strong>You can change your selection from these categories:</strong>
 
      {% available_categories as available %}
 
      {% include "registrasion/_category_list.html" with categories=available exclude=missing %}
 
    </p>
 
    {% available_categories as available %}
 
    {% if available|contains_items_not_in:missing %}
 
    <p><strong>You can change your selection from these categories:</strong></p>
 
    {% include "registrasion/_category_list.html" with categories=available exclude=missing %}
 
    {% endif %}
 
  </div>
 

	
 
  <div class="my-4">
 
    <h2>Voucher</h2>
 
    <h3>Voucher</h3>
 
    <p>If you have been given a voucher, please <a id="voucher-form-button" href="{% url "voucher_code" %}">enter your voucher code</a> now.
 
    </p>
 
  </div>
 

	
 
  <div class="my-4">
 
    <h2>What next?</h2>
 
    <h3>What next?</h3>
 
    {% if pending %}
 
    <p>You can either check out an invoice and pay for your selections, or return to
 
        the dashboard.</p>
 

	
 
    <a class="btn btn-primary" href="{% url "checkout" %}">
 
      <i class="fa fa-credit-card"></i> Check out and pay
 
    </a>
 

	
 
    <a class="btn btn-light" href="{% url "dashboard" %}">Return to dashboard</a>
 

	
 
    {% else %}
 

	
 
    <p>You have no items that need to be paid.</p>
 

	
 
    <div class="form-actions">
 
      <a class="btn btn-light" href="{% url "dashboard" %}">Return to dashboard</a>
 
    </div>
 

	
 
    {% endif %}
 
  </div>
 

	
 
{% endblock %}
pinaxcon/templates/registrasion/voucher_code.html
Show inline comments
 
{% extends "registrasion/base.html" %}
 
{% load crispy_forms_tags %}
 

	
 
{% block head_title %}Apply Voucher{% endblock %}
 
{% block page_title %}Apply Voucher{% endblock %}
 

	
 
{% block proposals_body %}
 
  <form method="post" action="">
 
    {% csrf_token %}
 

	
 
    <div class="panel panel-primary">
 
      <div class="panel-body">
 
        {{ voucher_form|crispy}}
 
      </div>
 

	
 
      <div class="panel-footer">
 
        <input class="btn btn-primary" type="submit" value="Apply voucher code" />
 
        <a href="{% url "dashboard" %}" class="btn btn-default">Back to dashboard</a>
 
      </div>
 
    </div>
 
  </form>
 

	
 
{% endblock %}
pinaxcon/templates/symposion/dashboard/_categories.html
Show inline comments
 
{% load i18n %}
 
{% load proposal_tags %}
 
{% load review_tags %}
 
{% load teams_tags %}
 
{% load registrasion_tags %}
 
{% load lca2018_tags %}
 
{% load lca2019_tags %}
 
{% load staticfiles %}
 
{% load waffle_tags %}
 

	
 
{% if user.is_staff %}
 
<div class="mb-4">
 
  <div class="row">
 
    <div class="col-12">
 
      <h2>{% trans "Administration" %}</h2>
 
      <p>The following administrative tools are available to you:
 
        <ul class="list-unstyled">
 
          <li><a href="{% url "reports_list" %}">Reports</a></li>
 
        </ul>
 
      </p>
 
    </div>
 
  </div>
 
</div>
 
{% endif %}
 

	
 
<div class="mb-4">
 
  <div class="row">
 
    <div class="col-12">
 
      <h2>{% trans "Attend" %} {% conference_name %}</h2>
 
    </div>
 
  </div>
 

	
 
  {% if not user.attendee.completed_registration %}
 
  <div class="row">
 
    <div class="col-12">
 
      <h3>Register</h3>
 
      <p>To attend the conference, you must create an attendee profile and purchase your ticket</p>
 
      <div>
 
        <a class="btn btn-lg btn-primary" role="button" href="{% url "guided_registration" %}">Get your ticket</a>
 
      </div>
 
    </div>
 
  </div>
 
  {% else %}
 
  <div class="row">
 
    <div class="col-md-6 mb-3 mb-md-0">
 
      <h3>Attendee Profile</h3>
 
      <p>If you would like to change the details on your badge or your attendee statistics, you may edit your attendee profile here.</p>
 
      <div>
 
        <a class="btn btn-primary" role="button" href="{% url "attendee_edit" %}">Edit attendee profile</a>
 
        {% flag "badge_preview" %}
 
        <a class="btn btn-info" role="button" href="{% url "user_badge" %}">Preview my badge</a>
 
        {% endflag %}
 
      </div>
 
    </div>
 
    <div class="col-md-6 mb-3 mb-md-0">
 
      <h3>Account Management</h3>
 
      <p>If you would like to change your registered email address or password, you can use our self-service account management portal</p>
 
      <div>
 
        <a class="btn btn-primary" role="button" href="https://login.linux.conf.au/manage/">Account Management</a>
 
      </div>
 
    </div>
 
  </div>
 

	
 
  {% items_pending as pending %}
 
  <div class="row">
 
    <div class="col-12">
 
      <h3 class="my-3">Account</h3>
 
    </div>
 
  </div>
 

	
 
  <div class="row">
 
    {% if pending %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Items pending payment</h4>
 
      {% include "registrasion/_items_list.html" with items=pending %}
 
      <a class="btn btn-lg btn-primary" role="button" href="{% url "checkout" %}">Check out and pay</a>
 
      <a class="btn btn-primary" role="button" href="{% url "checkout" %}"><i class="fa fa-credit-card"></i> Check out and pay</a>
 
      <a class="btn btn-secondary" role="button" href="{% url "voucher_code" %}">Apply voucher</a>
 
    </div>
 
    {% endif %}
 

	
 
    {% items_purchased as purchased %}
 
    {% if purchased %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Paid Items</h4>
 
      {% include "registrasion/_items_list.html" with items=purchased %}
 

	
 
      {% if not pending %}
 
      <a class="btn btn-secondary" role="button" href="{% url "voucher_code" %}">Apply voucher</a>
 
      {% endif %}
 
    </div>
 
    {% endif %}
 

	
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Add/Update Items</h4>
 
      {% include "registrasion/_category_list.html" with categories=categories %}
 
      {% missing_categories as missing %}
 
      {% if missing %}
 
      <div class="alert alert-warning my-4 pb-4">
 
        <h5 class="alert-heading">You have empty categories</h5>
 
        <p>You have <em>not</em> selected anything from the following
 
          categories. If your ticket includes any of these, you still need to
 
          make a selection:
 
        </p>
 

	
 
        {% include "registrasion/_category_list.html" with categories=missing %}
 
      </div>
 
      {% endif %}
 

	
 
      {% available_categories as available %}
 
      {% if available|contains_items_not_in:missing %}
 
      <p><strong>You can change your selection from these categories:</strong></p>
 
      {% include "registrasion/_category_list.html" with categories=available exclude=missing %}
 
      {% endif %}
 
    </div>
 

	
 
    {% invoices as invoices %}
 
    {% if invoices %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Invoices</h4>
 
      <ul>
 
        {% for invoice in invoices %}
 
        <li{% if invoice.is_void %} class="void-invoice" style="display: none;"{% endif %}>
 
          <a href="{% url "invoice" invoice.id %}" >Invoice {{ invoice.id }}</a> - ${{ invoice.value }} ({{ invoice.get_status_display }})
 
        </li>
 
        {% endfor %}
 
      </ul>
 
      {% if invoices|any_is_void %}
 
      <div class="mt-auto">
 
        <button id="toggle-void-invoices" onclick="toggleVoidInvoices();" class="btn btn-lg btn-default">Show void invoices</button>
 
        <button type="button" class="btn btn-sm btn-outline-dark" id="toggle-void-invoices" onclick="toggleVoidInvoices();">Show void invoices</button>
 
      </div>
 
      {% endif %}
 
    </div>
 
    {% endif %}
 

	
 
    {% if false %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Raffle Tickets</h4>
 

	
 
      <p><a href="/raffle/tickets/">View all my raffle tickets</a></p>
 
      {# REMOVE HARDCODED CATEGORY NUMBER!!!! #}
 
      <p><a href="/tickets/category/8">Buy raffle tickets</a></p>
 
    </div>
 
    {% endif %}
 

	
 
    {% available_credit as credit %}
 
    {% if credit %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Credit</h4>
 
      <p>You have ${{ credit }} leftover from refunded invoices. This credit will be automatically applied to new invoices. Contact the conference organisers if you wish to arrange a refund to your original payment source.</p>
 
    </div>
 
    {% endif %}
 
  </div>
 
  {% endif %} {# user.attendee.completed_registration #}
 
</div>
pinaxcon/templatetags/lca2019_tags.py
Show inline comments
 
from django import template
 
from django.forms import Form
 
import re
 

	
 

	
 
register = template.Library()
 

	
 

	
 
@register.filter
 
def has_required_fields(form):
 
    for field in form:
 
        if isinstance(field, Form):
 
            if has_required_fields(field):
 
                return True
 
        if field.field.required:
 
            return True
 
    return False
 

	
 

	
 
@register.filter
 
def has_price_fields(form):
 
    for field in form:
 
        if isinstance(field, Form):
 
            return has_price_fields(field)
 

	
 
        help_text = field.field.help_text or ''
 
        if '$' in help_text:
 
            return True
 

	
 
        label = field.field.label or ''
 
        if '$' in label:
 
            return True
 

	
 
        choices = getattr(field.field, 'choices', [])
 
        if choices:
 
            for choice_id, choice_text in choices:
 
                if '$' in choice_text:
 
                    return True
 

	
 
    return False
 

	
 

	
 
@register.filter
 
def any_is_void(invoices):
 
    for invoice in invoices:
 
        if invoice.is_void:
 
            return True
 
    return False
 

	
 

	
 
@register.filter
 
def contains_items_not_in(list1, list2):
 
    return len(set(list1).difference(list2)) > 0
 

	
 

	
 
@register.filter
 
def listlookup(lookup, target):
 
    try:
 
        return lookup[target]
 
    except IndexError:
 
        return None
 

	
 

	
 
@register.filter
 
def clean_text(txt):
 
    # Remove double/triple/+ spaces from `txt` and replace with single space
 
    return re.sub(r' {2,}' , ' ', txt)
 

	
 

	
 
@register.filter
 
def twitter_handle(txt):
 
    # Add @ to twitter handle if not present
 
    return txt if txt.startswith('@') else '@{}'.format(txt)
vendor/registrasion/registrasion/reporting/views.py
Show inline comments
 
from . import forms
 

	
 
import collections
 
import datetime
 
import itertools
 

	
 
from django.conf import settings
 
from django.contrib.auth.decorators import user_passes_test
 
from django.contrib.auth.models import User
 
from django.core.urlresolvers import reverse
 
from django.db import models
 
from django.db.models import F, Q, Subquery, OuterRef
 
from django.db.models import Count, Max, Sum
 
from django.db.models import Case, When, Value
 
from django.db.models.fields.related import RelatedField
 
from django.db.models.fields import CharField
 
from django.shortcuts import render
 

	
 
from registrasion.controllers.cart import CartController
 
from registrasion.controllers.item import ItemController
 
from registrasion.models import conditions
 
from registrasion.models import commerce
 
from registrasion.models import people
 
from registrasion import util
 
from registrasion import views
 

	
 
from symposion.schedule import models as schedule_models
 

	
 
from .reports import get_all_reports
 
from .reports import Links
 
from .reports import ListReport
 
from .reports import QuerysetReport
 
from .reports import report_view
 

	
 
import bleach
 

	
 

	
 
def CURRENCY():
 
    return models.DecimalField(decimal_places=2)
 

	
 

	
 
AttendeeProfile = util.get_object_from_name(settings.ATTENDEE_PROFILE_MODEL)
 

	
 

	
 
@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__,
 
            "title": report.title,
 
            "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("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(),
 
    ]
 

	
 

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

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

	
 
    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 ListReport("Items sold", headings, data)
 

	
 

	
 
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(
 
            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("Limits")
 
def limits(request, form):
 
    ''' Shows the summary of sales against stock limits. '''
 

	
 
    product_items = commerce.ProductItem.objects.filter(
 
        cart__status=commerce.Invoice.STATUS_PAID,
 
    ).values(
 
        "product", "product__name",
 
    ).annotate(
 
        total_quantity=Sum("quantity")
 
    )
 

	
 
    limits = conditions.TimeOrStockLimitFlag.objects.all().order_by("-limit")
 

	
 
    headings = ["Product", "Quantity"]
 

	
 
    reports = []
 
    for limit in limits:
 
        data = []
 
        total = 0
 
        limit_items = product_items.filter(product__in=limit.all_products)
 
        for product in limit_items:
 
            data.append([product['product__name'], product['total_quantity']])
 
            total += product['total_quantity']
 
        if limit.limit:
 
            data.append(['(TOTAL)', '%s/%s' % (total, limit.limit)])
 
        else:
 
            data.append(['(TOTAL)', total])
 

	
 
        description = limit.description
 
        extras = []
 
        if limit.start_time:
 
            extras.append('Starts: %s' % (limit.start_time))
 

	
 
        if limit.end_time:
 
            extras.append('Ends: %s' % (limit.end_time))
 

	
 
        if extras:
 
            description += ' (' + ', '.join(extras) + ')'
 

	
 
        reports.append(ListReport(description, headings, data))
 

	
 
    # now get discount items
 
    discount_items = conditions.DiscountBase.objects.select_subclasses()
 

	
 
    data = []
 
    for discount in discount_items.all():
 
        quantity = 0
 
        for item in discount.discountitem_set.filter(cart__status=2):
 
            quantity += item.quantity
 

	
 
        description = discount.description
 
        extras = []
 
        if getattr(discount, 'start_time', None):
 
            extras.append('Starts: %s' % (discount.start_time))
 

	
 
        if getattr(discount, 'end_time', None):
 
            extras.append('Ends: %s' % (discount.end_time))
 

	
 
        if extras:
 
            description += ' (' + ', '.join(extras) + ')'
 

	
 
        if getattr(discount, 'limit', None):
 
            data.append([description, '%s/%s' % (quantity, discount.limit)])
 
        else:
 
            data.append([description, quantity])
 

	
 
    headings = ["Discount", "Quantity"]
 
    reports.append(ListReport('Discounts', headings, data))
 

	
 
    vprod=Subquery(conditions.VoucherFlag.objects.filter(
 
        voucher__code__in=OuterRef("cart__vouchers__code")).values("products"))
 
    vcat=Subquery(conditions.VoucherFlag.objects.filter(
 
        voucher__code__in=OuterRef("cart__vouchers__code")).values("categories"))
 
    invoices_using_vouchers=commerce.Invoice.objects.filter((
 
        Q(lineitem__product__in=vprod)|
 
        Q(lineitem__product__category__in=vcat)
 
    )).filter(
 
        status=commerce.Invoice.STATUS_PAID).order_by(
 
            "cart__vouchers__code").select_related("user")
 

	
 
    vouchers = []
 
    for inv in invoices_using_vouchers:
 
        code=inv.cart.vouchers.all().values("code")[0]["code"]
 
        vouchers.append((code,inv.id,inv))
 
    voucher_headings = ["Code","Invoice#","Details"]
 
    reports.append(ListReport('Vouchers', voucher_headings, vouchers))
 

	
 
    return reports
 

	
 

	
 
@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"],
 
        ])
 

	
 
    return ListReport("Inventory", headings, data)
 

	
 

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

	
 

	
 
@report_view("Product Line Items By Date & Customer", form_type=forms.ProductAndCategoryForm)
 
def product_line_items(request, form):
 
    ''' Shows each product line item from invoices, including their date and
 
    purchasing customer. '''
 

	
 
    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)
 
        ),
 
        status=commerce.Invoice.STATUS_PAID,
 
    ).select_related(
 
        "cart",
 
        "user",
 
        "user__attendee",
 
        "user__attendee__attendeeprofilebase"
 
    ).order_by("issue_time").distinct()
 

	
 
    headings = [
 
        'Invoice', 'Invoice Date', 'Attendee', 'Qty', 'Product', 'Status'
 
    ]
 

	
 
    data = []
 
    for invoice in invoices:
 
        for item in invoice.cart.productitem_set.all():
 
            if item.product in products or item.product.category in categories:
 
                output = []
 
                output.append(invoice.id)
 
                output.append(invoice.issue_time.strftime('%Y-%m-%d %H:%M:%S'))
 
                output.append(
 
                    invoice.user.attendee.attendeeprofilebase.attendee_name()
 
                )
 
                output.append(item.quantity)
 
                output.append(item.product)
 
                cart = invoice.cart
 
                if cart.status == commerce.Cart.STATUS_PAID:
 
                    output.append('PAID')
 
                elif cart.status == commerce.Cart.STATUS_ACTIVE:
 
                    output.append('UNPAID')
 
                elif cart.status == commerce.Cart.STATUS_RELEASED:
 
                    output.append('REFUNDED')
 
                data.append(output)
 

	
 
    return ListReport("Line Items", headings, data)
 

	
 

	
 
@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)
 
        ),
 
        status=commerce.Invoice.STATUS_PAID,
 
    )
 

	
 
    # Invoices with payments will be paid at the time of their latest payment
 
    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")
 
    )
 

	
 
    # Zero-value invoices will have no payments, so they're paid at issue time
 
    zero_value_invoices = invoices.filter(value=0).distinct()
 

	
 
    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:
 
        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,
 
    )
 

	
 

	
 
@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"],
 
        notes,
 
        headings=["id", "Owner", "Status", "Value"],
 
        link_view=views.credit_note,
 
    )
 

	
 

	
 
@report_view("Invoices")
 
def invoices(request, form):
 
    ''' Shows all of the invoices in the system. '''
 

	
 
    invoices = commerce.Invoice.objects.all().order_by("status", "id")
 

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

	
 
    if user_id is None:
 
        return attendee_list(request)
 

	
 
    reports = []
 

	
 
    profile_data = []
 
    try:
 
        attendee = people.Attendee.objects.get(user__id=user_id)
 
    except people.DoesNotExist:
 
        return reports
 

	
 
    profile_data = []
 
    try:
 
        name = attendee.attendeeprofilebase.attendee_name()
 

	
 
        profile = people.AttendeeProfileBase.objects.get_subclass(
 
            attendee=attendee
 
        )
 
        fields = profile._meta.get_fields()
 
    except people.AttendeeProfileBase.DoesNotExist:
 
        name = attendee.user.username
 
        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())
 
        elif isinstance(field, CharField):
 
            try:
 
                value = bleach.clean(str(value))
 
            except TypeError:
 
                value = "Bad value for %s" % field.name
 

	
 
        profile_data.append((field.verbose_name, value))
 

	
 
    cart = CartController.for_user(attendee.user)
 
    try:
 
        reservation = cart.cart.reservation_duration + cart.cart.time_last_updated
 
    except AttributeError:  # No reservation_duration set -- default to 24h
 
        reservation = datetime.datetime.now() + datetime.timedelta(hours=24)
 

	
 
    profile_data.append(("Current cart reserved until", reservation))
 

	
 
    reports.append(ListReport("Profile", ["", ""], profile_data))
 

	
 
    links = []
 
    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))
 

	
 
    # Paid and pending  products
 
    try:
 
        ic = ItemController(attendee.user)
 
        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()],
 
        ))
 
    except AttributeError:
 
        pass
 

	
 
    # Invoices
 
    try:
 
        invoices = commerce.Invoice.objects.filter(
 
            user=attendee.user,
 
        )
 
        reports.append(QuerysetReport(
 
            "Invoices",
 
            ["id", "get_status_display", "value"],
 
            invoices,
 
            headings=["Invoice ID", "Status", "Value"],
 
            link_view=views.invoice,
 
        ))
 
    except AttrbuteError:
 
        pass
 

	
 
    # Credit Notes
 
    try:
 
        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,
 
        ))
 
    except AttributeError:
 
        pass
 

	
 
    # All payments
 
    try:
 
        payments = commerce.PaymentBase.objects.filter(
 
            invoice__user=attendee.user,
 
        ).select_related("invoice")
 

	
 
        reports.append(QuerysetReport(
 
            "Payments",
 
            ["invoice__id", "id", "reference", "amount"],
 
            payments,
 
            link_view=views.invoice,
 
        ))
 
    except AttributeError:
 
        pass
 

	
 
    return reports
 

	
 

	
 
def attendee_list(request):
 
    ''' Returns a list of all attendees. '''
 

	
 
    attendees = people.Attendee.objects.select_related(
 
        "attendeeprofilebase",
 
        "user",
 
    )
 

	
 
    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,
 
            (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")
 

	
 
    # Add invoice nag link
 
    links = []
 
    invoice_mailout = reverse(views.invoice_mailout, args=[])
 
    invoice_mailout += "?" + request.META["QUERY_STRING"]
 
    links += [
 
        (invoice_mailout + "&status=1", "Send invoice reminders",),
 
        (invoice_mailout + "&status=2", "Send mail for paid invoices",),
 
    ]
 

	
 
    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"  # noqa
 
    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], )  # noqa
 
    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])  # noqa
 

	
 
    # Group the responses per-field.
 
    for field in fields:
 
        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:
 
            # 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 bleach.clean(str(value))
 

	
 
        status_count = lambda status: Case(When(  # noqa
 
                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, ))
 
        ).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),
 
                    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
 
    ]
 

	
 
    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 ""
 
        else:
 
            try:
 
                return bleach.clean(str(attr))
 
            except TypeError:
 
                return "Bad value found for %s" % attr
 

	
 
    headings = ["User ID", "Name", "Email", "Product", "Item Status"]
 
    headings.extend(field_names)
 
    data = []
 
    for item in items:
 
        profile = by_user[item.cart.user]
 
        line = [
 
            item.cart.user.id,
 
            getattr(profile, name_field),
 
            profile.attendee.user.email,
 
            item.product,
 
            status_display[item.cart.status],
 
        ] + [
 
            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(
 
        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)
 

	
 
    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))
 
    users = users.order_by("paid_carts")
 

	
 
    return QuerysetReport(
 
        "Speaker Registration Status",
 
        ["id", "speaker_profile__name", "email", "paid_carts"],
 
        users,
 
        link_view=attendee,
 
    )
 

	
 
    return []
 

	
 

	
 
@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", "Checkin", "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]
 
        if hasattr(user, "checkin") and hasattr(user.checkin, "checkin_code"):
 
            code = user.checkin.checkin_code
 
        else:
 
            code = None
 
        output.append([
 
            user.id,
 
            user.attendee.attendeeprofilebase.attendee_name(),
 
            code,
 
            format_items(items["paid"]),
 
            format_items(items["unpaid"]),
 
            format_items(items["refunded"]),
 
        ])
 

	
 
    return ListReport("Manifest", headings, output)
 

	
 
    # attendeeprofilebase.attendee_name()
vendor/registrasion/registrasion/views.py
Show inline comments
...
 
@@ -312,1088 +312,1089 @@ def _guided_registration_products(request, mode):
 
            prefix = "category_" + str(category.id)
 
            p = _handle_products(request, category, products, prefix)
 
            products_form, discounts, products_handled = p
 

	
 
            section = GuidedRegistrationSection(
 
                title=category.name,
 
                description=category.description,
 
                discounts=discounts,
 
                form=products_form,
 
            )
 

	
 
            if products:
 
                # This product category has items to show.
 
                sections.append(section)
 
                seen_categories.append(category)
 

	
 
    # Update the cache with the newly calculated values
 
    cat_ids = [cat.id for cat in seen_categories]
 
    request.session[SESSION_KEY] = {MODE_KEY: mode, CATS_KEY: cat_ids}
 

	
 
    return sections
 

	
 

	
 
@login_required
 
def _guided_registration_profile_and_voucher(request):
 
    voucher_form, voucher_handled = _handle_voucher(request, "voucher")
 
    profile_form, profile_handled = _handle_profile(request, "profile")
 

	
 
    voucher_section = GuidedRegistrationSection(
 
        title="Voucher Code",
 
        form=voucher_form,
 
    )
 

	
 
    profile_section = GuidedRegistrationSection(
 
        title="Profile and Personal Information",
 
        form=profile_form,
 
        description=("<div class=\"text-info\"><em>You can come back and edit these details any time before "
 
                     "January 6 2020.</em></div>"),
 
    )
 

	
 
    return [voucher_section, profile_section]
 

	
 

	
 
@login_required
 
def review(request):
 
    ''' View for the review page. '''
 

	
 
    return render(
 
        request,
 
        "registrasion/review.html",
 
        {},
 
    )
 

	
 

	
 
@login_required
 
def edit_profile(request):
 
    ''' View for editing an attendee's profile
 

	
 
    The user must be logged in to edit their profile.
 

	
 
    Returns:
 
        redirect or render:
 
            In the case of a ``POST`` request, it'll redirect to ``dashboard``,
 
            or otherwise, it will render ``registrasion/profile_form.html``
 
            with data::
 

	
 
                {
 
                    "form": form,  # Instance of ATTENDEE_PROFILE_FORM.
 
                }
 

	
 
    '''
 

	
 
    if hasattr(request.user, "checkin"):
 
        if request.user.checkin.checked_in_bool:
 
            messages.add_message(
 
                request, messages.ERROR,
 
                'Profile cannot be edited. Please email contact@lca2020.linux.org.au '
 
                'if you need any changes to your profile.')
 
            return redirect(reverse('dashboard'))
 

	
 
    form, handled = _handle_profile(request, "profile")
 

	
 
    if handled and not form.errors:
 
        messages.success(
 
            request,
 
            "Your attendee profile was updated.",
 
        )
 
        return redirect("dashboard")
 

	
 
    data = {
 
        "form": form,
 
    }
 
    return render(request, "registrasion/profile_form.html", data)
 

	
 

	
 
# Define the attendee profile form, or get a default.
 
try:
 
    ProfileForm = util.get_object_from_name(settings.ATTENDEE_PROFILE_FORM)
 
except:
 
    class ProfileForm(django_forms.ModelForm):
 
        class Meta:
 
            model = util.get_object_from_name(settings.ATTENDEE_PROFILE_MODEL)
 
            exclude = ["attendee"]
 

	
 

	
 
def _handle_profile(request, prefix):
 
    ''' Returns a profile form instance, and a boolean which is true if the
 
    form was handled. '''
 
    attendee = people.Attendee.get_instance(request.user)
 

	
 
    try:
 
        profile = attendee.attendeeprofilebase
 
        profile = people.AttendeeProfileBase.objects.get_subclass(
 
            pk=profile.id,
 
        )
 
    except ObjectDoesNotExist:
 
        profile = None
 

	
 
    # Load a pre-entered name from the speaker's profile,
 
    # if they have one.
 
    try:
 
        speaker_profile = request.user.speaker_profile
 
        speaker_name = speaker_profile.name
 
    except ObjectDoesNotExist:
 
        speaker_name = None
 

	
 
    name_field = ProfileForm.Meta.model.name_field()
 
    initial = {}
 
    if profile is None and name_field is not None:
 
        initial[name_field] = speaker_name
 

	
 
    form = ProfileForm(
 
        request.POST or None,
 
        initial=initial,
 
        instance=profile,
 
        prefix=prefix
 
    )
 

	
 
    handled = True if request.POST else False
 

	
 
    if request.POST and form.is_valid():
 
        form.instance.attendee = attendee
 
        form.save()
 

	
 
    return form, handled
 

	
 

	
 
@login_required
 
def product_category(request, category_id):
 
    ''' Form for selecting products from an individual product category.
 

	
 
    Arguments:
 
        category_id (castable to int): The id of the category to display.
 

	
 
    Returns:
 
        redirect or render:
 
            If the form has been sucessfully submitted, redirect to
 
            ``dashboard``. Otherwise, render
 
            ``registrasion/product_category.html`` with data::
 

	
 
                {
 
                    "category": category,         # An inventory.Category for
 
                                                  # category_id
 
                    "discounts": discounts,       # A list of
 
                                                  # DiscountAndQuantity
 
                    "form": products_form,        # A form for selecting
 
                                                  # products
 
                    "voucher_form": voucher_form, # A form for entering a
 
                                                  # voucher code
 
                }
 

	
 
    '''
 

	
 
    PRODUCTS_FORM_PREFIX = "products"
 
    VOUCHERS_FORM_PREFIX = "vouchers"
 

	
 
    # Handle the voucher form *before* listing products.
 
    # Products can change as vouchers are entered.
 
    v = _handle_voucher(request, VOUCHERS_FORM_PREFIX)
 
    voucher_form, voucher_handled = v
 

	
 
    category_id = int(category_id)  # Routing is [0-9]+
 
    category = inventory.Category.objects.get(pk=category_id)
 

	
 
    with BatchController.batch(request.user):
 
        products = ProductController.available_products(
 
            request.user,
 
            category=category,
 
        )
 

	
 
        if not products:
 
            messages.warning(
 
                request,
 
                (
 
                    "There are no products available from category: " +
 
                    category.name
 
                ),
 
            )
 
            return redirect("dashboard")
 

	
 
        p = _handle_products(request, category, products, PRODUCTS_FORM_PREFIX)
 
        products_form, discounts, products_handled = p
 

	
 
    if request.POST and not voucher_handled and not products_form.errors:
 
        # Only return to the dashboard if we didn't add a voucher code
 
        # and if there's no errors in the products form
 
        if products_form.has_changed():
 
            messages.success(
 
                request,
 
                "Your reservations have been updated.",
 
            )
 
        return redirect(review)
 

	
 
    data = {
 
        "category": category,
 
        "discounts": discounts,
 
        "form": products_form,
 
        "voucher_form": voucher_form,
 
    }
 

	
 
    return render(request, "registrasion/product_category.html", data)
 

	
 

	
 
def voucher_code(request):
 
    ''' A view *just* for entering a voucher form. '''
 

	
 
    VOUCHERS_FORM_PREFIX = "vouchers"
 

	
 
    # Handle the voucher form *before* listing products.
 
    # Products can change as vouchers are entered.
 
    v = _handle_voucher(request, VOUCHERS_FORM_PREFIX)
 
    voucher_form, voucher_handled = v
 

	
 
    if voucher_handled:
 
        messages.success(request, "Your voucher code was accepted.")
 
        return redirect("dashboard")
 

	
 
    data = {
 
        "voucher_form": voucher_form,
 
    }
 

	
 
    return render(request, "registrasion/voucher_code.html", data)
 

	
 

	
 

	
 
def _handle_products(request, category, products, prefix):
 
    ''' Handles a products list form in the given request. Returns the
 
    form instance, the discounts applicable to this form, and whether the
 
    contents were handled. '''
 

	
 
    current_cart = CartController.for_user(request.user)
 

	
 
    ProductsForm = forms.ProductsForm(category, products)
 

	
 
    # Create initial data for each of products in category
 
    items = commerce.ProductItem.objects.filter(
 
        product__in=products,
 
        cart=current_cart.cart,
 
    ).select_related("product")
 
    quantities = []
 
    seen = set()
 

	
 
    for item in items:
 
        quantities.append((item.product, item.quantity))
 
        seen.add(item.product)
 

	
 
    zeros = set(products) - seen
 
    for product in zeros:
 
        quantities.append((product, 0))
 

	
 
    products_form = ProductsForm(
 
        request.POST or None,
 
        product_quantities=quantities,
 
        prefix=prefix,
 
    )
 

	
 
    if request.method == "POST" and products_form.is_valid():
 
        if products_form.has_changed():
 
            _set_quantities_from_products_form(products_form, current_cart)
 

	
 
        # If category is required, the user must have at least one
 
        # in an active+valid cart
 
        if category.required:
 
            carts = commerce.Cart.objects.filter(user=request.user,
 
                                                 status=commerce.Cart.STATUS_ACTIVE)
 
            items = commerce.ProductItem.objects.filter(
 
                product__category=category,
 
                cart=current_cart.cart,
 
            )
 

	
 
            if len(items) == 0:
 
                products_form.add_error(
 
                    None,
 
                    "You must have at least one item from this category",
 
                )
 
    handled = False if products_form.errors else True
 

	
 
    # Making this a function to lazily evaluate when it's displayed
 
    # in templates.
 

	
 
    discounts = util.lazy(
 
        DiscountController.available_discounts,
 
        request.user,
 
        [],
 
        products,
 
    )
 

	
 
    return products_form, discounts, handled
 

	
 

	
 
def _set_quantities_from_products_form(products_form, current_cart):
 

	
 
    # Makes id_to_quantity, a dictionary from product ID to its quantity
 
    quantities = list(products_form.product_quantities())
 
    id_to_quantity = dict(quantities)
 

	
 
    # Get the actual product objects
 
    pks = [i[0] for i in quantities]
 
    products = inventory.Product.objects.filter(
 
        id__in=pks,
 
    ).select_related("category").order_by("id")
 

	
 
    quantities.sort(key=lambda i: i[0])
 

	
 
    # Match the product objects to their quantities
 
    product_quantities = [
 
        (product, id_to_quantity[product.id]) for product in products
 
    ]
 

	
 
    try:
 
        current_cart.set_quantities(product_quantities)
 
    except CartValidationError as ve:
 
        for ve_field in ve.error_list:
 
            product, message = ve_field.message
 
            products_form.add_product_error(product, message)
 

	
 

	
 
def _handle_voucher(request, prefix):
 
    ''' Handles a voucher form in the given request. Returns the voucher
 
    form instance, and whether the voucher code was handled. '''
 

	
 
    voucher_form = forms.VoucherForm(request.POST or None, prefix=prefix)
 
    current_cart = CartController.for_user(request.user)
 

	
 
    if (voucher_form.is_valid() and
 
            voucher_form.cleaned_data["voucher"].strip()):
 

	
 
        voucher = voucher_form.cleaned_data["voucher"]
 
        voucher = inventory.Voucher.normalise_code(voucher)
 

	
 
        if len(current_cart.cart.vouchers.filter(code=voucher)) > 0:
 
            # This voucher has already been applied to this cart.
 
            # Do not apply code
 
            handled = False
 
        else:
 
            try:
 
                current_cart.apply_voucher(voucher)
 
            except Exception as e:
 
                voucher_form.add_error("voucher", e)
 
            handled = True
 
    else:
 
        handled = False
 

	
 
    return (voucher_form, handled)
 

	
 

	
 
@login_required
 
def checkout(request, user_id=None):
 
    ''' Runs the checkout process for the current cart.
 

	
 
    If the query string contains ``fix_errors=true``, Registrasion will attempt
 
    to fix errors preventing the system from checking out, including by
 
    cancelling expired discounts and vouchers, and removing any unavailable
 
    products.
 

	
 
    Arguments:
 
        user_id (castable to int):
 
            If the requesting user is staff, then the user ID can be used to
 
            run checkout for another user.
 
    Returns:
 
        render or redirect:
 
            If the invoice is generated successfully, or there's already a
 
            valid invoice for the current cart, redirect to ``invoice``.
 
            If there are errors when generating the invoice, render
 
            ``registrasion/checkout_errors.html`` with the following data::
 

	
 
                {
 
                    "error_list", [str, ...]  # The errors to display.
 
                }
 

	
 
    '''
 

	
 
    if user_id is not None:
 
        if request.user.is_staff:
 
            user = User.objects.get(id=int(user_id))
 
        else:
 
            raise Http404()
 
    else:
 
        user = request.user
 

	
 
    current_cart = CartController.for_user(user)
 

	
 
    fix_errors = request.GET.get("fix_errors") == "true"
 
    if fix_errors:
 
        current_cart.fix_simple_errors()
 

	
 
    try:
 
        current_invoice = InvoiceController.for_cart(current_cart.cart)
 
    except ValidationError as ve:
 
        return _checkout_errors(request, ve)
 

	
 
    if fix_errors:
 
        current_invoice.update_recipient()
 

	
 
    return redirect("invoice", current_invoice.invoice.id)
 

	
 

	
 
def _checkout_errors(request, errors):
 

	
 
    error_list = []
 
    for error in errors.error_list:
 
        if isinstance(error, tuple):
 
            error = error[1]
 
        error_list.append(error)
 

	
 
    data = {
 
        "error_list": error_list,
 
    }
 

	
 
    return render(request, "registrasion/checkout_errors.html", data)
 

	
 

	
 
def invoice_access(request, access_code):
 
    ''' Redirects to an invoice for the attendee that matches the given access
 
    code, if any.
 

	
 
    If the attendee has multiple invoices, we use the following tie-break:
 

	
 
    - If there's an unpaid invoice, show that, otherwise
 
    - If there's a paid invoice, show the most recent one, otherwise
 
    - Show the most recent invoid of all
 

	
 
    Arguments:
 

	
 
        access_code (castable to int): The access code for the user whose
 
            invoice you want to see.
 

	
 
    Returns:
 
        redirect:
 
            Redirect to the selected invoice for that user.
 

	
 
    Raises:
 
        Http404: If the user has no invoices.
 
    '''
 

	
 
    invoices = commerce.Invoice.objects.filter(
 
        user__attendee__access_code=access_code,
 
    ).order_by("-issue_time")
 

	
 
    if not invoices:
 
        raise Http404()
 

	
 
    unpaid = invoices.filter(status=commerce.Invoice.STATUS_UNPAID)
 
    paid = invoices.filter(status=commerce.Invoice.STATUS_PAID)
 

	
 
    if unpaid:
 
        invoice = unpaid[0]  # (should only be 1 unpaid invoice?)
 
    elif paid:
 
        invoice = paid[0]  # Most recent paid invoice
 
    else:
 
        invoice = invoices[0]  # Most recent of any invoices
 

	
 
    return redirect("invoice", invoice.id, access_code)
 

	
 

	
 
def invoice(request, invoice_id, access_code=None):
 
    ''' Displays an invoice.
 

	
 
    This view is not authenticated, but it will only allow access to either:
 
    the user the invoice belongs to; staff; or a request made with the correct
 
    access code.
 

	
 
    Arguments:
 

	
 
        invoice_id (castable to int): The invoice_id for the invoice you want
 
            to view.
 

	
 
        access_code (Optional[str]): The access code for the user who owns
 
            this invoice.
 

	
 
    Returns:
 
        render:
 
            Renders ``registrasion/invoice.html``, with the following
 
            data::
 

	
 
                {
 
                    "invoice": models.commerce.Invoice(),
 
                }
 

	
 
    Raises:
 
        Http404: if the current user cannot view this invoice and the correct
 
            access_code is not provided.
 

	
 
    '''
 

	
 
    current_invoice = InvoiceController.for_id_or_404(invoice_id)
 

	
 
    if not current_invoice.can_view(
 
            user=request.user,
 
            access_code=access_code,
 
            ):
 
        raise Http404()
 

	
 
    data = {
 
        "invoice": current_invoice.invoice,
 
    }
 

	
 
    return render(request, "registrasion/invoice.html", data)
 

	
 

	
 
def invoice_update(request, invoice_id):
 
    ''' Updates an invoice to have the latest recipient information. '''
 

	
 
    current_invoice = InvoiceController.for_id_or_404(invoice_id)
 

	
 
    if not current_invoice.can_view(user=request.user):
 
        raise Http404()
 

	
 
    current_invoice.update_recipient()
 

	
 
    return redirect("invoice", current_invoice.invoice.id)
 

	
 

	
 
def _staff_only(user):
 
    ''' Returns true if the user is staff. '''
 
    return user.is_staff
 

	
 

	
 
@user_passes_test(_staff_only)
 
def manual_payment(request, invoice_id):
 
    ''' Allows staff to make manual payments or refunds on an invoice.
 

	
 
    This form requires a login, and the logged in user needs to be staff.
 

	
 
    Arguments:
 
        invoice_id (castable to int): The invoice ID to be paid
 

	
 
    Returns:
 
        render:
 
            Renders ``registrasion/manual_payment.html`` with the following
 
            data::
 

	
 
                {
 
                    "invoice": models.commerce.Invoice(),
 
                    "form": form,   # A form that saves a ``ManualPayment``
 
                                    # object.
 
                }
 

	
 
    '''
 

	
 
    FORM_PREFIX = "manual_payment"
 

	
 
    current_invoice = InvoiceController.for_id_or_404(invoice_id)
 

	
 
    form = forms.ManualPaymentForm(
 
        request.POST or None,
 
        prefix=FORM_PREFIX,
 
    )
 

	
 
    if request.POST and form.is_valid():
 
        form.instance.invoice = current_invoice.invoice
 
        form.instance.entered_by = request.user
 
        form.save()
 
        current_invoice.update_status()
 
        form = forms.ManualPaymentForm(prefix=FORM_PREFIX)
 

	
 
    data = {
 
        "invoice": current_invoice.invoice,
 
        "form": form,
 
    }
 

	
 
    return render(request, "registrasion/manual_payment.html", data)
 

	
 

	
 
@user_passes_test(_staff_only)
 
def refund(request, invoice_id):
 
    ''' Marks an invoice as refunded and requests a credit note for the
 
    full amount paid against the invoice.
 

	
 
    This view requires a login, and the logged in user must be staff.
 

	
 
    Arguments:
 
        invoice_id (castable to int): The ID of the invoice to refund.
 

	
 
    Returns:
 
        redirect:
 
            Redirects to ``invoice``.
 

	
 
    '''
 

	
 
    current_invoice = InvoiceController.for_id_or_404(invoice_id)
 

	
 
    try:
 
        current_invoice.refund()
 
        messages.success(request, "This invoice has been refunded.")
 
    except ValidationError as ve:
 
        messages.error(request, ve)
 

	
 
    return redirect("invoice", invoice_id)
 

	
 

	
 
@user_passes_test(_staff_only)
 
def credit_note(request, note_id, access_code=None):
 
    ''' Displays a credit note.
 

	
 
    If ``request`` is a ``POST`` request, forms for applying or refunding
 
    a credit note will be processed.
 

	
 
    This view requires a login, and the logged in user must be staff.
 

	
 
    Arguments:
 
        note_id (castable to int): The ID of the credit note to view.
 

	
 
    Returns:
 
        render or redirect:
 
            If the "apply to invoice" form is correctly processed, redirect to
 
            that invoice, otherwise, render ``registration/credit_note.html``
 
            with the following data::
 

	
 
                {
 
                    "credit_note": models.commerce.CreditNote(),
 
                    "apply_form": form,  # A form for applying credit note
 
                                         # to an invoice.
 
                    "refund_form": form, # A form for applying a *manual*
 
                                         # refund of the credit note.
 
                    "cancellation_fee_form" : form, # A form for generating an
 
                                                    # invoice with a
 
                                                    # cancellation fee
 
                }
 

	
 
    '''
 

	
 
    note_id = int(note_id)
 
    current_note = CreditNoteController.for_id_or_404(note_id)
 

	
 
    apply_form = forms.ApplyCreditNoteForm(
 
        current_note.credit_note.invoice.user,
 
        request.POST or None,
 
        prefix="apply_note"
 
    )
 

	
 
    refund_form = forms.ManualCreditNoteRefundForm(
 
        request.POST or None,
 
        prefix="refund_note"
 
    )
 

	
 
    cancellation_fee_form = forms.CancellationFeeForm(
 
        request.POST or None,
 
        prefix="cancellation_fee"
 
    )
 

	
 
    if request.POST and apply_form.is_valid():
 
        inv_id = apply_form.cleaned_data["invoice"]
 
        invoice = commerce.Invoice.objects.get(pk=inv_id)
 
        current_note.apply_to_invoice(invoice)
 
        messages.success(
 
            request,
 
            "Applied credit note %d to invoice." % note_id,
 
        )
 
        return redirect("invoice", invoice.id)
 

	
 
    elif request.POST and refund_form.is_valid():
 
        refund_form.instance.entered_by = request.user
 
        refund_form.instance.parent = current_note.credit_note
 
        refund_form.save()
 
        messages.success(
 
            request,
 
            "Applied manual refund to credit note."
 
        )
 
        refund_form = forms.ManualCreditNoteRefundForm(
 
            prefix="refund_note",
 
        )
 

	
 
    elif request.POST and cancellation_fee_form.is_valid():
 
        percentage = cancellation_fee_form.cleaned_data["percentage"]
 
        invoice = current_note.cancellation_fee(percentage)
 
        messages.success(
 
            request,
 
            "Generated cancellation fee for credit note %d." % note_id,
 
        )
 
        return redirect("invoice", invoice.invoice.id)
 

	
 
    data = {
 
        "credit_note": current_note.credit_note,
 
        "apply_form": apply_form,
 
        "refund_form": refund_form,
 
        "cancellation_fee_form": cancellation_fee_form,
 
    }
 

	
 
    return render(request, "registrasion/credit_note.html", data)
 

	
 

	
 
@user_passes_test(_staff_only)
 
def amend_registration(request, user_id):
 
    ''' Allows staff to amend a user's current registration cart, and etc etc.
 
    '''
 

	
 
    user = User.objects.get(id=int(user_id))
 
    current_cart = CartController.for_user(user)
 

	
 
    voucher_form = forms.VoucherForm(
 
        request.POST or None,
 
        prefix="voucher",
 
    )
 

	
 
    if request.POST and voucher_form.has_changed() and voucher_form.is_valid():
 
        try:
 
            current_cart.apply_voucher(voucher_form.cleaned_data["voucher"])
 
            return redirect(amend_registration, user_id)
 
        except ValidationError as ve:
 
            voucher_form.add_error(None, ve)
 

	
 

	
 
    items = commerce.ProductItem.objects.filter(
 
        cart=current_cart.cart,
 
    ).select_related("product")
 
    initial = [{"product": i.product, "quantity": i.quantity} for i in items]
 

	
 
    StaffProductsFormSet = forms.staff_products_formset_factory(user)
 
    formset = StaffProductsFormSet(
 
        request.POST or None,
 
        initial=initial,
 
        prefix="products",
 
    )
 

	
 
    for item, form in zip(items, formset):
 
        queryset = inventory.Product.objects.filter(id=item.product.id)
 
        form.fields["product"].queryset = queryset
 

	
 
    if request.POST and formset.is_valid():
 

	
 
        pq = [
 
            (f.cleaned_data["product"], f.cleaned_data["quantity"])
 
            for f in formset
 
            if "product" in f.cleaned_data and
 
            f.cleaned_data["product"] is not None
 
        ]
 

	
 
        try:
 
            current_cart.set_quantities(pq)
 
            return redirect(amend_registration, user_id)
 
        except ValidationError as ve:
 
            for ve_field in ve.error_list:
 
                product, message = ve_field.message
 
                for form in formset:
 
                    if "product" not in form.cleaned_data:
 
                        # This is the empty form.
 
                        continue
 
                    if form.cleaned_data["product"] == product:
 
                        form.add_error("quantity", message)
 

	
 
    ic = ItemController(user)
 
    data = {
 
        "user": user,
 
        "paid": ic.items_purchased(),
 
        "cancelled": ic.items_released(),
 
        "form": formset,
 
        "voucher_form": voucher_form,
 
        "vouchers": current_cart.cart.vouchers.all(),
 
    }
 

	
 
    return render(request, "registrasion/amend_registration.html", data)
 

	
 

	
 
@user_passes_test(_staff_only)
 
def extend_reservation(request, user_id, days=7):
 
    ''' Allows staff to extend the reservation on a given user's cart.
 
    '''
 

	
 
    user = User.objects.get(id=int(user_id))
 
    cart = CartController.for_user(user)
 
    cart.extend_reservation(datetime.timedelta(days=days))
 

	
 
    return redirect(request.META["HTTP_REFERER"])
 

	
 

	
 
Email = namedtuple(
 
    "Email",
 
    ("subject", "body", "from_email", "recipient_list"),
 
)
 

	
 

	
 
@user_passes_test(_staff_only)
 
def invoice_mailout(request):
 
    ''' Allows staff to send emails to users based on their invoice status. '''
 

	
 
    category = request.GET.getlist("category", [])
 
    product = request.GET.getlist("product", [])
 
    status = request.GET.get("status")
 

	
 
    form = forms.InvoiceEmailForm(
 
        request.POST or None,
 
        category=category,
 
        product=product,
 
        status=status,
 
    )
 

	
 
    emails = []
 

	
 
    if form.is_valid():
 
        emails = []
 
        for invoice in form.cleaned_data["invoice"]:
 
            # datatuple = (subject, message, from_email, recipient_list)
 
            from_email = form.cleaned_data["from_email"]
 
            subject = form.cleaned_data["subject"]
 
            body = Template(form.cleaned_data["body"]).render(
 
                Context({
 
                    "invoice": invoice,
 
                    "user": invoice.user,
 
                })
 
            )
 
            recipient_list = [invoice.user.email]
 
            emails.append(Email(subject, body, from_email, recipient_list))
 

	
 
        if form.cleaned_data["action"] == forms.InvoiceEmailForm.ACTION_SEND:
 
            # Send e-mails *ONLY* if we're sending.
 
            send_mass_mail(emails)
 
            messages.info(request, "The e-mails have been sent.")
 

	
 
    data = {
 
        "form": form,
 
        "emails": emails,
 
    }
 

	
 
    return render(request, "registrasion/invoice_mailout.html", data)
 

	
 
def _get_badge_template_name():
 
    return os.path.join(settings.PROJECT_ROOT, 'pinaxcon', 'templates',
 
                        settings.BADGER_DEFAULT_SVG)
 
@login_required
 
def user_badge(request, format="png"):
 
    '''Shows the logged-in user their badge'''
 
    if waffle.flag_is_active(request, "force_badge_overlay"):
 
        overlay=True
 
    else:
 
        overlay=False
 
    return render_badge(request.user, format, overlay, request)
 

	
 
@user_passes_test(_staff_only)
 
def badge_print(request, user_id, format="pdf"):
 
    ''' Renders a single user's badge overlay (PDF). '''
 

	
 
    user_id = int(user_id)
 
    user = User.objects.get(pk=user_id)
 

	
 
    return render_badge(user, format=format, overlay=True)
 

	
 
@user_passes_test(_staff_only)
 
def badge(request, user_id, format="png"):
 
    ''' Renders a single user's badge (PNG). '''
 

	
 
    user_id = int(user_id)
 
    user = User.objects.get(pk=user_id)
 

	
 
    return render_badge(user, format=format)
 

	
 
def _convert_img(img, outformat="png", dpi=300, width=None):
 
    if hasattr(img, "encode"): #a string, or a SafeText
 
        img=img.encode()
 

	
 
    width = width or dpi*5.83 #5.83 inches in A5 portrait
 
    conversion_cmdline = [
 
        "convert",
 
        "-density", str(dpi),
 
        "-resize", str(width),
 
        "-units", "PixelsPerInch"
 
        ]
 
    if outformat=="png":
 
        conversion_cmdline.extend(["-background","white",
 
                                   "-flatten"])
 
    conversion_cmdline.extend(["-", "{}:-".format(outformat)])
 
    if outformat=="pdf":
 
        # Inkscape results in nicer pdf render, embedding fonts etc where possible
 
        conversion_cmdline = [
 
            'inkscape',
 
            '-f', '/dev/stdin',
 
            '-A', '/dev/stdout',
 
            '-d', str(dpi)
 
        ]
 

	
 
    return subprocess.run(conversion_cmdline, input=img, stdout=subprocess.PIPE).stdout
 

	
 
def render_badge(user, format="png", overlay=False, request=None):
 
    if request and waffle.flag_is_active(request, "force_badge_overlay"):
 
        overlay=True
 

	
 
    rendered = render_badge_svg(user, overlay=overlay)
 
    if format == "png":
 
        rendered = _convert_img(rendered, outformat="png")
 
    elif format == "pdf":
 
        rendered = _convert_img(rendered, outformat="pdf")
 

	
 
    response = HttpResponse(rendered)
 

	
 
    if format == "svg":
 
        response["Content-Type"] = "image/svg+xml;charset=utf-8"
 
        response["Content-Disposition"] = 'inline; filename="badge.svg"'
 
    elif format == "png":
 
        response["Content-Type"] = "image/png"
 
        response["Content-Disposition"] = 'inline; filename="badge.png"'
 
    elif format == "pdf":
 
        response["Content-Type"] = "application/pdf"
 
        response["Content-Disposition"] = 'inline; filename="badge.pdf"'
 

	
 
    return response
 

	
 
def render_badge_svg(user, overlay=False):
 
    ''' Renders a single user's badge as SVG. '''
 

	
 
    data = {
 
        "user": user,
 
        "overlay": overlay
 
    }
 

	
 
    if overlay:
 
        template = 'registrasion/badge_overlay.svg'
 
    else:
 
        template = 'registrasion/badge.svg'
 

	
 
    t = loader.get_template(template)
 
    return t.render(data)
 

	
 
def badges(request):
 
    '''
 
    *** NOT USED FOR PYCONAU 2017 MELBOURNE  (I.e., not supported in badger module.) ***
 

	
 
    Either displays a form containing a list of users with badges to
 
    render, or returns a .zip file containing their badges. '''
 

	
 
    category = request.GET.getlist("category", [])
 
    product = request.GET.getlist("product", [])
 
    status = request.GET.get("status")
 

	
 
    form = forms.InvoicesWithProductAndStatusForm(
 
        request.POST or None,
 
        category=category,
 
        product=product,
 
        status=status,
 
    )
 

	
 
    if form.is_valid():
 
        response = HttpResponse()
 
        response["Content-Type"] = "application.zip"
 
        response["Content-Disposition"] = 'attachment; filename="badges.zip"'
 

	
 
        z = zipfile.ZipFile(response, "w")
 

	
 
        for invoice in form.cleaned_data["invoice"]:
 
            user = invoice.user
 
            badge = render_badge(user)
 
            z.writestr("badge_%d.svg" % user.id, badge.encode("utf-8"))
 

	
 
        return response
 

	
 
    data = {
 
        "form": form,
 
    }
 

	
 
    return render(request, "registrasion/badges.html", data)
 

	
 

	
 
def collate_from_form(form):
 
    '''
 
    Does what collate does, but using form data as its input source
 
    rather than User record.
 
    '''
 
        # Build the thing we'll pass to svg_badge() later.
 
    data = dict()
 

	
 
    # Get the name bits ...
 
    at_nm = form.data['name'].split()
 
    if at_nm[0].lower() in 'mr dr ms mrs miss'.split():
 
        at_nm[0] = at_nm[0] + ' ' + at_nm[1]
 
        del at_nm[1]
 
    if at_nm:
 
        data['firstname'] = at_nm[0]
 
        data['lastname'] = ''.join(at_nm[1:])
 
    else:  # Can't happen -- form validator will check for this.
 
        pass
 

	
 
    # Free text -- only one line ... come on!
 
    data['line1'] = form.data['free_text_1']
 
    data['line2'] = form.data['free_text_2']
 

	
 
    # Email ...
 
    data['email'] = form.data['email']
 

	
 
    # Don't think we want to allow ad hoc organiser tickets ...
 
    data['organiser'] = False
 

	
 
    # Punt on shirts for now ...
 
    data['shirts'] = list()
 

	
 
    # Lots booleans ...
 
    for key in ['over18', 'paid', 'friday', 'speaker', 'tutorial',
 
                'sprints', 'company',]:
 
        data[key] = form.data.get(key, False)
 

	
 
    # This will throw InvalidTicketChoiceError if the ticket
 
    # choice isn't found in the ticket list or is the
 
    # "Plese select a valid tickt" choice.  (I.e., they forgot
 
    # to choose a ticket.)
 
    data['ticket'] = ticket_selection()[int(form.data['ticket'])][1]
 

	
 
    data['volunteer'] = data['ticket'].find("Volunteer") >= 0
 

	
 
    if 'Specialist Day Only' in data['ticket']:
 
        data['ticket'] = 'Friday Only'
 
        data['friday'] = True
 

	
 
    if 'Conference Organiser' in data['ticket']:
 
        data['ticket'] = ''
 

	
 
    if 'Conference Volunteer' in data['ticket']:
 
        data['ticket'] = ''
 

	
 
    data['promote_company'] = (
 
        data['organiser'] or data['volunteer'] or data['speaker'] or
 
        'Sponsor' in data['ticket'] or
 
        'Contributor' in data['ticket'] or
 
        'Professional' in data['ticket']
 
    )
 

	
 
    return data
 

	
 

	
 
@user_passes_test(_staff_only)
 
def badger(request, username=None):
 
    '''
 
    Renders a single user's badge from data supplied on
 
    a form rather than from Attendee data.
 

	
 
    If *username* is provided in the URL, an attempt
 
    will be made to look up this user and fill in the
 
    badge details from the User and Attendee records.
 
    '''
 

	
 
    if username is not None:
 

	
 
        # We have a username.  Try to populate our badge data
 
        # from User/Attendee model.
 
        try:
 
            data = collate({'usernames': [username,]}).next()
 
        except: # No matching User record (probably) ... just put up a blank form
 
            return render(request, settings.BADGER_DEFAULT_FORM, {'form': BadgeForm})
 
    else:
 
        form = BadgeForm(request.POST)
 

	
 
        if len(form.data) == 0:  # Empty or request to put up the form.
 
            return render(request, settings.BADGER_DEFAULT_FORM, {'form': BadgeForm})
 

	
 
        try:
 
            if form.is_valid():
 
               data = collate_from_form(form)
 

	
 
        except InvalidTicketChoiceError:
 
            form.add_error('ticket', 'Please select a VALID ticket type!')
 
            return render(request, settings.BADGER_DEFAULT_FORM, {'form': form})
 

	
 
        except TypeError as e:
 
            form.add_error(e.message)
 
            return render(request, settings.BADGER_DEFAULT_FORM, {'form': form})
 

	
 

	
 
    # We should have valid data if we get this far.
 
    # Fill in the template and return the resulting SVG object.
 
    orig = etree.parse(_get_badge_template_name())
 
    tree = deepcopy(orig)
 
    root = tree.getroot()
 

	
 
    # Generate the badge (svg)
 
    svg_badge(root, data, 0)
 

	
 
    # Ship it back to the user...
 
    response = HttpResponse(etree.tostring(root))
 

	
 
    response["Content-Type"] = "image/svg+xml"
 
    response["Content-Disposition"] = 'inline; filename="badge.svg"'
 
    return response
0 comments (0 inline, 0 general)