diff --git a/registrasion/forms.py b/registrasion/forms.py index 4839b733..d852a0d1 100644 --- a/registrasion/forms.py +++ b/registrasion/forms.py @@ -4,6 +4,7 @@ from registrasion.models import inventory from django import forms from django.core.exceptions import ValidationError +from django.db.models import Q class ApplyCreditNoteForm(forms.Form): @@ -14,22 +15,39 @@ class ApplyCreditNoteForm(forms.Form): self.user = user super(ApplyCreditNoteForm, self).__init__(*a, **k) - self.fields["invoice"].choices = self._unpaid_invoices_for_user + self.fields["invoice"].choices = self._unpaid_invoices - def _unpaid_invoices_for_user(self): + def _unpaid_invoices(self): invoices = commerce.Invoice.objects.filter( status=commerce.Invoice.STATUS_UNPAID, - user=self.user, - ) + ).select_related("user") + invoices_annotated = [invoice.__dict__ for invoice in invoices] + users = dict((inv.user.id, inv.user) for inv in invoices) + for invoice in invoices_annotated: + invoice.update({ + "user_id": users[invoice["user_id"]].id, + "user_email": users[invoice["user_id"]].email, + }) + print invoice + + + key = lambda inv: (0 - (inv["user_id"] == self.user.id), inv["id"]) + invoices_annotated.sort(key=key) + + template = "Invoice %(id)d - user: %(user_email)s (%(user_id)d) - $%(value)d" return [ - (invoice.id, "Invoice %(id)d - $%(value)d" % invoice.__dict__) - for invoice in invoices + (invoice["id"], template % invoice) + for invoice in invoices_annotated ] invoice = forms.ChoiceField( required=True, ) + verify = forms.BooleanField( + required=True, + help_text="Have you verified that this is the correct invoice?", + ) class CancellationFeeForm(forms.Form): @@ -394,3 +412,39 @@ def staff_products_formset_factory(user): ''' Creates a formset of StaffProductsForm for the given user. ''' form_type = staff_products_form_factory(user) return forms.formset_factory(form_type) + + +class InvoiceNagForm(forms.Form): + invoice = forms.ModelMultipleChoiceField( + widget=forms.CheckboxSelectMultiple, + queryset=commerce.Invoice.objects.all(), + ) + from_email = forms.CharField() + subject = forms.CharField() + body = forms.CharField( + widget=forms.Textarea, + ) + + def __init__(self, *a, **k): + category = k.pop('category', None) or [] + product = k.pop('product', None) or [] + + category = [int(i) for i in category] + product = [int(i) for i in product] + + super(InvoiceNagForm, self).__init__(*a, **k) + + qs = commerce.Invoice.objects.filter( + status=commerce.Invoice.STATUS_UNPAID, + ).filter( + Q(lineitem__product__category__in=category) | + Q(lineitem__product__in=product) + ) + + # Uniqify + qs = commerce.Invoice.objects.filter( + id__in=qs, + ) + + self.fields['invoice'].queryset = qs + self.fields['invoice'].initial = [i.id for i in qs] diff --git a/registrasion/models/commerce.py b/registrasion/models/commerce.py index 8c8bcd9b..72ece2c8 100644 --- a/registrasion/models/commerce.py +++ b/registrasion/models/commerce.py @@ -152,7 +152,9 @@ class Invoice(models.Model): ] def __str__(self): - return "Invoice #%d" % self.id + return "Invoice #%d (to: %s, due: %s, value: %s)" % ( + self.id, self.user.email, self.due_time, self.value + ) def clean(self): if self.cart is not None and self.cart_revision is None: diff --git a/registrasion/reporting/reports.py b/registrasion/reporting/reports.py index eb2c33a6..00861072 100644 --- a/registrasion/reporting/reports.py +++ b/registrasion/reporting/reports.py @@ -213,8 +213,21 @@ def report_view(title, form_type=None): class ReportView(object): + ''' View objects that can render report data into HTML or CSV. ''' def __init__(self, inner_view, title, form_type): + ''' + + Arguments: + inner_view: Callable that returns either a Report or a sequence of + Report objects. + + title: The title that appears at the top of all of the reports. + + form_type: A Form class that can be used to query the report. + + ''' + # Consolidate form_type so it has content type and section self.inner_view = inner_view self.title = title @@ -226,6 +239,8 @@ class ReportView(object): def get_form(self, request): + ''' Creates an instance of self.form_type using request.GET ''' + # Create a form instance if self.form_type is not None: form = self.form_type(request.GET) @@ -239,6 +254,10 @@ class ReportView(object): @classmethod def wrap_reports(cls, reports, content_type): + ''' Wraps the reports in a _ReportTemplateWrapper for the given + content_type -- this allows data to be returned as HTML links, for + instance. ''' + reports = [ _ReportTemplateWrapper(content_type, report) for report in reports @@ -247,6 +266,16 @@ class ReportView(object): return reports def render(self, data): + ''' Renders the reports based on data.content_type's value. + + Arguments: + data (ReportViewRequestData): The report data. data.content_type + is used to determine how the reports are rendered. + + Returns: + HTTPResponse: The rendered version of the report. + + ''' renderers = { "text/csv": self._render_as_csv, "text/html": self._render_as_html, @@ -280,8 +309,20 @@ class ReportView(object): class ReportViewRequestData(object): + ''' + + Attributes: + form (Form): form based on request + reports ([Report, ...]): The reports rendered from the request + + Arguments: + report_view (ReportView): The ReportView to call back to. + request (HTTPRequest): A django HTTP request + + ''' def __init__(self, report_view, request, *a, **k): + self.report_view = report_view self.request = request @@ -293,6 +334,9 @@ class ReportViewRequestData(object): self.section = request.GET.get("section") self.section = int(self.section) if self.section else None + if self.content_type is None: + self.content_type = "text/html" + # Reports come from calling the inner view reports = report_view.inner_view(request, self.form, *a, **k) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index 2aaa3c31..a350cb3d 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -408,11 +408,13 @@ 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) - if form.cleaned_data["user"] is not None: - user_id = form.cleaned_data["user"] + print user_id attendee = people.Attendee.objects.get(user__id=user_id) name = attendee.attendeeprofilebase.attendee_name() @@ -589,6 +591,16 @@ def attendee_data(request, form, user_id=None): "cart", "cart__user", "product", "product__category", ).order_by("cart__status") + # Add invoice nag link + links = [] + links.append(( + reverse(views.nag_unpaid, args=[]) + "?" + request.META["QUERY_STRING"], + "Send invoice reminders", + )) + + 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 diff --git a/registrasion/urls.py b/registrasion/urls.py index 0d21854c..2028d86b 100644 --- a/registrasion/urls.py +++ b/registrasion/urls.py @@ -13,6 +13,7 @@ from .views import ( invoice, invoice_access, manual_payment, + nag_unpaid, product_category, refund, review, @@ -34,6 +35,7 @@ public = [ refund, name="refund"), url(r"^invoice_access/([A-Z0-9]+)$", invoice_access, name="invoice_access"), + url(r"^nag_unpaid$", nag_unpaid, name="nag_unpaid"), url(r"^profile$", edit_profile, name="attendee_edit"), url(r"^register$", guided_registration, name="guided_registration"), url(r"^review$", review, name="review"), diff --git a/registrasion/views.py b/registrasion/views.py index 8b0d2b5d..5f57eb20 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -26,9 +26,11 @@ from django.contrib.auth.models import User from django.contrib import messages from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError +from django.core.mail import send_mass_mail from django.http import Http404 from django.shortcuts import redirect from django.shortcuts import render +from django.template import Context, Template _GuidedRegistrationSection = namedtuple( @@ -916,3 +918,39 @@ def extend_reservation(request, user_id, days=7): cart.extend_reservation(datetime.timedelta(days=days)) return redirect(request.META["HTTP_REFERER"]) + + +@user_passes_test(_staff_only) +def nag_unpaid(request): + ''' Allows staff to nag users with unpaid invoices. ''' + + category = request.GET.getlist("category", []) + product = request.GET.getlist("product", []) + + form = forms.InvoiceNagForm( + request.POST or None, + category=category, + product=product, + ) + + 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, + }) + ) + recipient_list = [invoice.user.email] + emails.append((subject, body, from_email, recipient_list)) + send_mass_mail(emails) + messages.info(request, "The e-mails have been sent.") + + data = { + "form": form, + } + + return render(request, "registrasion/nag_unpaid.html", data)