Add basic ability to cancel registrations without invoices/refunds
authorMagnus Hagander <magnus@hagander.net>
Thu, 8 Mar 2018 15:20:46 +0000 (10:20 -0500)
committerMagnus Hagander <magnus@hagander.net>
Thu, 8 Mar 2018 15:20:46 +0000 (10:20 -0500)
For registrations that have a single invoice *and* are eligible for a
refund, we already had working cancelation. But it did not work for bulk
invoices or non-refundable cancelations, which both required manual work
through /admin/ to cancel them. This still does not automate bulk
invoice refunding, but at least provides the ability to perform such
refunds as two-step operations, and it fully supports canceling without
refund.

This also adds sending of email on all cancelations, both those that are
done without refund, and those that are done with a refund like before.
Previously, attendees of those would only receive an invoice refund
notice. We now also send an explicit "your registration has been
canceled" email, if welcome emails have been enabled for the conference.

postgresqleu/confreg/invoicehandler.py
postgresqleu/confreg/util.py
postgresqleu/confreg/views.py
postgresqleu/urls.py
template/confreg/admin_registration_cancel.html [new file with mode: 0644]
template/confreg/admin_registration_cancel_confirm.html [new file with mode: 0644]
template/confreg/admin_registration_single.html
template/confreg/mail/reg_canceled.txt [new file with mode: 0644]

index 4ee041e282883ecb4b1a832d9e2ad38829414d5d..8f434f073822dcef9178ce8d17a5c29d1994b58e 100644 (file)
@@ -3,7 +3,7 @@ from django.conf import settings
 from postgresqleu.mailqueue.util import send_template_mail, send_simple_mail
 from models import ConferenceRegistration, BulkPayment, PendingAdditionalOrder
 from models import RegistrationWaitlistHistory, PrepaidVoucher
-from util import notify_reg_confirmed, expire_additional_options
+from util import notify_reg_confirmed, expire_additional_options, cancel_registration
 
 from datetime import datetime
 
@@ -83,21 +83,7 @@ class InvoiceProcessor(object):
                except ConferenceRegistration.DoesNotExist:
                        raise Exception("Could not find conference registration %s" % invoice.processorid)
 
-               if not reg.payconfirmedat:
-                       raise Exception("Registration not paid, data is out of sync!")
-
-               # Unlink this invoice from the registration, and remove the payment
-               # confirmation. This will automatically "unlock" the registration.
-               reg.invoice = None
-               reg.payconfirmedat = None
-               reg.payconfirmedby = None
-               reg.save()
-
-               # Once unlinked, remove the registration as well. If we don't
-               # do this, the user will get notifications to remember to
-               # complete their registration in the future, and that will be
-               # confusing.
-               reg.delete()
+               cancel_registration(reg)
 
        # Return the user to a page showing what happened as a result
        # of their payment. In our case, we just return the user directly
index 67c1646c950e010433548f7f4221aa9122c047ae..6008c827b627f7a80087b8e9796022eda42f5494 100644 (file)
@@ -148,6 +148,48 @@ def notify_reg_confirmed(reg, updatewaitlist=True):
        )
 
 
+def cancel_registration(reg):
+       # Verify that we're only canceling a real registration
+       if not reg.payconfirmedat:
+               raise Exception("Registration not paid, data is out of sync!")
+
+       # If we sent a welcome mail, also send a goodbye mail
+       if reg.conference.sendwelcomemail:
+               send_template_mail(reg.conference.contactaddr,
+                                                  reg.email,
+                                                  "[{0}] Registration canceled".format(reg.conference),
+                                                  'confreg/mail/reg_canceled.txt',
+                                                  {
+                                                          'conference': reg.conference,
+                                                          'reg': reg,
+                                                  },
+                                                  sendername=reg.conference.conferencename,
+                                                  receivername=reg.fullname,
+               )
+
+       # Now actually delete the reg. Start by unlinking things that might be there.
+       if reg.vouchercode:
+               if PrepaidVoucher.objects.filter(user=reg).exists():
+                       v = PrepaidVoucher.objects.get(user=reg)
+                       v.user = None
+                       v.usedate = None
+                       v.save()
+               elif DiscountCode.objects.filter(registrations=reg).exists():
+                       d = DiscountCode.objects.get(registrations=reg)
+                       d.registrations.remove(reg)
+                       d.save()
+       reg.invoice = None
+       reg.payconfirmedat = None
+       reg.payconfirmedby = None
+       reg.save()
+
+       # Once unlinked, remove the registration as well. If we don't
+       # do this, the user will get notifications to remember to
+       # complete their registration in the future, and that will be
+       # confusing.
+       reg.delete()
+
+
 
 def get_invoice_autocancel(*args):
        # Each argument is expected to be an integer with number of hours,
index 40b283db861e02eb44de71493e75664e54c2013b..c55f7ba4cf06e8f3842ba6a46d73293febdfa61a 100644 (file)
@@ -36,7 +36,7 @@ from forms import AttendeeMailForm, WaitlistOfferForm, TransferRegForm
 from forms import NewMultiRegForm, MultiRegInvoiceForm
 from forms import SessionSlidesUrlForm, SessionSlidesFileForm
 from util import invoicerows_for_registration, notify_reg_confirmed, InvoicerowsException
-from util import get_invoice_autocancel
+from util import get_invoice_autocancel, cancel_registration
 
 from models import get_status_string
 from regtypes import confirm_special_reg_type, validate_special_reg_type
@@ -2497,6 +2497,28 @@ def admin_registration_single(request, urlname, regid):
                'signups': _get_registration_signups(conference, reg),
        }, RequestContext(request))
 
+@login_required
+@transaction.atomic
+def admin_registration_cancel(request, urlname, regid):
+       if request.user.is_superuser:
+               conference = get_object_or_404(Conference, urlname=urlname)
+       else:
+               conference = get_object_or_404(Conference, urlname=urlname, administrators=request.user)
+
+       reg = get_object_or_404(ConferenceRegistration, id=regid, conference=conference)
+
+       if request.method == 'POST' and request.POST.get('docancel') == '1':
+               name = reg.fullname
+               cancel_registration(reg)
+               return render_to_response('confreg/admin_registration_cancel_confirm.html', {
+                       'conference': conference,
+                       'name': name,
+               })
+       else:
+               return render_to_response('confreg/admin_registration_cancel.html', {
+                       'conference': conference,
+                       'reg': reg,
+               }, RequestContext(request))
 
 @login_required
 @transaction.atomic
index aee6c7c898c01f736d44742b71f44108bd941184..71fedf640aaf41aa982096e0c8d6db0dc4d29773 100644 (file)
@@ -118,6 +118,7 @@ urlpatterns = patterns('',
     (r'^events/admin/(\w+)/regdashboard/$', postgresqleu.confreg.views.admin_registration_dashboard),
     (r'^events/admin/(\w+)/regdashboard/list/$', postgresqleu.confreg.views.admin_registration_list),
     (r'^events/admin/(\w+)/regdashboard/list/(\d+)/$', postgresqleu.confreg.views.admin_registration_single),
+    (r'^events/admin/(\w+)/regdashboard/list/(\d+)/cancel/$', postgresqleu.confreg.views.admin_registration_cancel),
     (r'^events/admin/(\w+)/waitlist/$', postgresqleu.confreg.views.admin_waitlist),
     (r'^events/admin/(\w+)/waitlist/cancel/(\d+)/$', postgresqleu.confreg.views.admin_waitlist_cancel),
     (r'^events/admin/(\w+)/wiki/$', postgresqleu.confwiki.views.admin),
diff --git a/template/confreg/admin_registration_cancel.html b/template/confreg/admin_registration_cancel.html
new file mode 100644 (file)
index 0000000..e6b75df
--- /dev/null
@@ -0,0 +1,45 @@
+{%extends "confreg/confadmin_base.html" %}
+{%load date_or_string%}
+{%block extrahead%}
+<script language="javascript">
+function confirmit() {
+   return confirm('Are you absolutely sure you want to cancel this registration? There is no way to roll i tback!');
+}
+</script>
+{%endblock%}
+
+{%block title%}Cancel registration{%endblock%}
+
+{%block layoutblock%}
+<h1>Cancel registration</h1>
+<h2>{{reg.fullname}}</h2>
+{%if reg.invoice%}
+<p>
+  This registration has an invoice attached to it. If you want to do a refund of this
+  invoice (full or partial), the cancellation must currently be done through the
+  invoice system.
+</p>
+<a class="btn btn-default btn-block" href="/invoiceadmin/{{reg.invoice.pk}}/refund/">Cancel with refund</a>
+{%elif reg.bulkpayment%}
+<p>
+  This registration is part of a bulk payment or a pay by somebody else invoice.
+  If you want to do a refund of this registration, has to be done through the
+  invoice system. However, in doing so the actual registration will not be canceled,
+  so you will <i>also</i> need to manually cancel thre reservation in question.
+</p>
+<a class="btn btn-default btn-block" href="/invoiceadmin/{{reg.bulkpayment.invoice.pk}}/refund/">Refund bulk invoice</a>
+{%else%}
+<p>
+  This registration does not have an invoice or bulk payment. That means it was either
+  a no-pay registration (such as voucher) or a manually confirmed one (speaker, staff,
+  or fully manual).
+</p>
+{%endif%}
+<form method="post" action=".">{% csrf_token %}
+  <input type="hidden" name="docancel" value="1">
+  <input type="submit" class="btn btn-default btn-block" value="Cancel registration without refund"  onclick="return confirmit()">
+</form>
+
+<a class="btn btn-default btn-block" href="/events/admin/{{conference.urlname}}/regdashboard/list/{{reg.id}}/">Back to registration</a>
+
+{%endblock%}
diff --git a/template/confreg/admin_registration_cancel_confirm.html b/template/confreg/admin_registration_cancel_confirm.html
new file mode 100644 (file)
index 0000000..48c5312
--- /dev/null
@@ -0,0 +1,21 @@
+{%extends "confreg/confadmin_base.html" %}
+{%load date_or_string%}
+{%block title%}Cancel registration{%endblock%}
+
+{%block layoutblock%}
+<h1>Cancel registration</h1>
+<p>
+  Registration for {{reg.fullname}} has been cancelled.
+</p>
+<p>
+{%if conference.sendwelcomemail%}
+An email has been sent to the attendee confirming the cancelation.
+{%else%}
+Since welcome email is not enabled for this conference, no
+email was sent to the attendee about the cancelation.
+{%endif%}
+</p>
+
+<a class="btn btn-default btn-block" href="/events/admin/{{conference.urlname}}/regdashboard/list/">Back to registration list</a>
+
+{%endblock%}
index 6c6c39cd85a1f44837b1cf60992ac03b985dca1e..078eb86474d21ebeba6b926b91e23d433492388b 100644 (file)
 
 </table>
 
+{%if reg.payconfirmedat%}
+<a class="btn btn-default btn-block" href="/events/admin/{{conference.urlname}}/regdashboard/list/{{reg.id}}/cancel/">Cancel registration</a>
+{%endif%}
+
 <a class="btn btn-default btn-block" href="/events/admin/{{conference.urlname}}/regdashboard/list/">Back to list</a>
 
 {%if user.is_superuser%}
diff --git a/template/confreg/mail/reg_canceled.txt b/template/confreg/mail/reg_canceled.txt
new file mode 100644 (file)
index 0000000..eb9c07f
--- /dev/null
@@ -0,0 +1,9 @@
+Your registration for {{conference.conferencename}} has been canceled.
+
+If you did not expect this or do not know why this happened,
+please contact us ASAP by responding to this email, and we will
+investigate the situation.
+
+Your registration has now been fully canceled, so you do not need
+to do anything else to complete it. We hope to see you again at
+a future event!