diff --git a/vendor/registrasion/.gitrepo b/vendor/registrasion/.gitrepo
index cc64f22f..c14eed14 100644
--- a/vendor/registrasion/.gitrepo
+++ b/vendor/registrasion/.gitrepo
@@ -6,6 +6,6 @@
 [subrepo]
 	remote = git@gitlab.com:tchaypo/registrasion.git
 	branch = lca2018
-	commit = c1e194aef92e4c06a8855fad22ca819c08736dad
-	parent = dd8a42e9f67cfebc39347cb87bacf79046bfbfec
+	commit = 7cf314adae9f8de1519db63828f55a10aa09f0ee
+	parent = 7c5c6c02f20ddcbc6c54b3065311880fce586192
 	cmdver = 0.3.1
diff --git a/vendor/registrasion/registrasion/controllers/category.py b/vendor/registrasion/registrasion/controllers/category.py
index 2b2b18d1..77875946 100644
--- a/vendor/registrasion/registrasion/controllers/category.py
+++ b/vendor/registrasion/registrasion/controllers/category.py
@@ -9,6 +9,8 @@ from django.db.models import Value
 
 from .batch import BatchController
 
+from operator import attrgetter
+
 
 class AllProducts(object):
     pass
@@ -26,7 +28,7 @@ class CategoryController(object):
         products, otherwise it'll do all. '''
 
         # STOPGAP -- this needs to be elsewhere tbqh
-        from registrasion.controllers.product import ProductController
+        from .product import ProductController
 
         if products is AllProducts:
             products = inventory.Product.objects.all().select_related(
@@ -38,7 +40,7 @@ class CategoryController(object):
             products=products,
         )
 
-        return set(i.category for i in available)
+        return sorted(set(i.category for i in available), key=attrgetter("order"))
 
     @classmethod
     @BatchController.memoise
diff --git a/vendor/registrasion/registrasion/controllers/credit_note.py b/vendor/registrasion/registrasion/controllers/credit_note.py
index 500d0ad7..6c2f7102 100644
--- a/vendor/registrasion/registrasion/controllers/credit_note.py
+++ b/vendor/registrasion/registrasion/controllers/credit_note.py
@@ -4,7 +4,7 @@ from django.db import transaction
 
 from registrasion.models import commerce
 
-from registrasion.controllers.for_id import ForId
+from .for_id import ForId
 
 
 class CreditNoteController(ForId, object):
@@ -40,8 +40,8 @@ class CreditNoteController(ForId, object):
         paid.
         '''
 
-        # Circular Import
-        from registrasion.controllers.invoice import InvoiceController
+        # Local import to fix import cycles. Can we do better?
+        from .invoice import InvoiceController
         inv = InvoiceController(invoice)
         inv.validate_allowed_to_pay()
 
@@ -65,8 +65,8 @@ class CreditNoteController(ForId, object):
         a cancellation fee. Must be 0 <= percentage <= 100.
         '''
 
-        # Circular Import
-        from registrasion.controllers.invoice import InvoiceController
+        # Local import to fix import cycles. Can we do better?
+        from .invoice import InvoiceController
 
         assert(percentage >= 0 and percentage <= 100)
 
diff --git a/vendor/registrasion/registrasion/controllers/flag.py b/vendor/registrasion/registrasion/controllers/flag.py
index b8d1b4e3..b64713e7 100644
--- a/vendor/registrasion/registrasion/controllers/flag.py
+++ b/vendor/registrasion/registrasion/controllers/flag.py
@@ -45,6 +45,28 @@ class FlagController(object):
         else:
             all_conditions = []
 
+        all_conditions = conditions.FlagBase.objects.filter(
+            id__in=set(i.id for i in all_conditions)
+        ).select_subclasses()
+
+        # Prefetch all of the products and categories (Saves a LOT of queries)
+        all_conditions = all_conditions.prefetch_related(
+            "products", "categories", "products__category",
+        )
+
+        # Now pre-select all of the products attached to those categories
+        all_categories = set(
+            cat for condition in all_conditions
+                for cat in condition.categories.all()
+        )
+        all_category_ids = (i.id for i in all_categories)
+        all_category_products = inventory.Product.objects.filter(
+            category__in=all_category_ids
+        ).select_related("category")
+
+        products_by_category_ = itertools.groupby(all_category_products, lambda prod: prod.category)
+        products_by_category = dict((k.id, list(v)) for (k, v) in products_by_category_)
+
         # All disable-if-false conditions on a product need to be met
         do_not_disable = defaultdict(lambda: True)
         # At least one enable-if-true condition on a product must be met
@@ -64,17 +86,19 @@ class FlagController(object):
             # Get all products covered by this condition, and the products
             # from the categories covered by this condition
 
-            ids = [product.id for product in products]
-
-            # TODO: This is re-evaluated a lot.
-            all_products = inventory.Product.objects.filter(id__in=ids)
-            cond = (
-                Q(flagbase_set=condition) |
-                Q(category__in=condition.categories.all())
+            condition_products = condition.products.all()
+            category_products = (
+                product for cat in condition.categories.all() for product in products_by_category[cat.id]
             )
 
-            all_products = all_products.filter(cond)
-            all_products = all_products.select_related("category")
+            all_products = itertools.chain(
+                condition_products, category_products
+            )
+            all_products = set(all_products)
+
+            # Filter out the products from this condition that
+            # are not part of this query.
+            all_products = set(i for i in all_products if i in products)
 
             if quantities:
                 consumed = sum(quantities[i] for i in all_products)
diff --git a/vendor/registrasion/registrasion/controllers/invoice.py b/vendor/registrasion/registrasion/controllers/invoice.py
index 706db7f9..8937843b 100644
--- a/vendor/registrasion/registrasion/controllers/invoice.py
+++ b/vendor/registrasion/registrasion/controllers/invoice.py
@@ -10,9 +10,9 @@ from registrasion.models import commerce
 from registrasion.models import conditions
 from registrasion.models import people
 
-from registrasion.controllers.cart import CartController
-from registrasion.controllers.credit_note import CreditNoteController
-from registrasion.controllers.for_id import ForId
+from .cart import CartController
+from .credit_note import CreditNoteController
+from .for_id import ForId
 
 
 class InvoiceController(ForId, object):
diff --git a/vendor/registrasion/registrasion/forms.py b/vendor/registrasion/registrasion/forms.py
index fec2ec79..3e0beaaa 100644
--- a/vendor/registrasion/registrasion/forms.py
+++ b/vendor/registrasion/registrasion/forms.py
@@ -1,6 +1,6 @@
-from registrasion.controllers.product import ProductController
-from registrasion.models import commerce
-from registrasion.models import inventory
+from .controllers.product import ProductController
+from .models import commerce
+from .models import inventory
 
 from django import forms
 from django.db.models import Q
@@ -31,12 +31,13 @@ class ApplyCreditNoteForm(forms.Form):
                 "user_email": users[invoice["user_id"]].email,
             })
 
-
         key = lambda inv: (0 - (inv["user_id"] == self.user.id), inv["id"])  # noqa
         invoices_annotated.sort(key=key)
 
-        template = ('Invoice %(id)d - user: %(user_email)s (%(user_id)d) '
-                    '-  $%(value)d')
+        template = (
+            'Invoice %(id)d - user: %(user_email)s (%(user_id)d) '
+            '-  $%(value)d'
+        )
         return [
             (invoice["id"], template % invoice)
             for invoice in invoices_annotated
@@ -94,6 +95,7 @@ def ProductsForm(category, products):
         cat.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm,
         cat.RENDER_TYPE_RADIO: _RadioButtonProductsForm,
         cat.RENDER_TYPE_ITEM_QUANTITY: _ItemQuantityProductsForm,
+        cat.RENDER_TYPE_CHECKBOX: _CheckboxProductsForm,
     }
 
     # Produce a subclass of _ProductsForm which we can alter the base_fields on
@@ -252,6 +254,35 @@ class _RadioButtonProductsForm(_ProductsForm):
         self.add_error(self.FIELD, error)
 
 
+class _CheckboxProductsForm(_ProductsForm):
+    ''' Products entry form that allows users to say yes or no
+    to desired products. Basically, it's a quantity form, but the quantity
+    is either zero or one.'''
+
+    @classmethod
+    def set_fields(cls, category, products):
+        for product in products:
+            field = forms.BooleanField(
+                label='%s -- %s' % (product.name, product.price),
+                required=False,
+            )
+            cls.base_fields[cls.field_name(product)] = field
+
+    @classmethod
+    def initial_data(cls, product_quantities):
+        initial = {}
+        for product, quantity in product_quantities:
+            initial[cls.field_name(product)] = bool(quantity)
+
+        return initial
+
+    def product_quantities(self):
+        for name, value in self.cleaned_data.items():
+            if name.startswith(self.PRODUCT_PREFIX):
+                product_id = int(name[len(self.PRODUCT_PREFIX):])
+                yield (product_id, int(value))
+
+
 class _ItemQuantityProductsForm(_ProductsForm):
     ''' Products entry form that allows users to select a product type, and
      enter a quantity of that product. This version _only_ allows a single
@@ -449,7 +480,6 @@ class InvoicesWithProductAndStatusForm(forms.Form):
         product = [int(i) for i in product]
 
         super(InvoicesWithProductAndStatusForm, self).__init__(*a, **k)
-        print(status)
 
         qs = commerce.Invoice.objects.filter(
             status=status or commerce.Invoice.STATUS_UNPAID,
diff --git a/vendor/registrasion/registrasion/migrations/0007_merge_20170929_2259.py b/vendor/registrasion/registrasion/migrations/0007_merge_20170929_2259.py
deleted file mode 100644
index bcd52f24..00000000
--- a/vendor/registrasion/registrasion/migrations/0007_merge_20170929_2259.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.5 on 2017-09-29 12:59
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('registrasion', '0006_auto_20170526_1624'),
-        ('registrasion', '0006_auto_20170702_2233'),
-    ]
-
-    operations = [
-    ]
diff --git a/vendor/registrasion/registrasion/models/__init__.py b/vendor/registrasion/registrasion/models/__init__.py
index 47efa127..3d247559 100644
--- a/vendor/registrasion/registrasion/models/__init__.py
+++ b/vendor/registrasion/registrasion/models/__init__.py
@@ -1,4 +1,4 @@
-from registrasion.models.commerce import *  # NOQA
-from registrasion.models.conditions import *  # NOQA
-from registrasion.models.inventory import *  # NOQA
-from registrasion.models.people import *  # NOQA
+from .commerce import *  # NOQA
+from .conditions import *  # NOQA
+from .inventory import *  # NOQA
+from .people import *  # NOQA
diff --git a/vendor/registrasion/registrasion/models/commerce.py b/vendor/registrasion/registrasion/models/commerce.py
index a392c96b..4791ec3d 100644
--- a/vendor/registrasion/registrasion/models/commerce.py
+++ b/vendor/registrasion/registrasion/models/commerce.py
@@ -324,7 +324,6 @@ class CreditNote(PaymentBase):
 
         elif hasattr(self, 'creditnoterefund'):
             reference = self.creditnoterefund.reference
-            print(reference)
             return "Refunded with reference: %s" % reference
 
         raise ValueError("This should never happen.")
diff --git a/vendor/registrasion/registrasion/models/inventory.py b/vendor/registrasion/registrasion/models/inventory.py
index 575566e2..6dfc6cd4 100644
--- a/vendor/registrasion/registrasion/models/inventory.py
+++ b/vendor/registrasion/registrasion/models/inventory.py
@@ -42,6 +42,8 @@ class Category(models.Model):
             have a lot of options, from which the user is not going to select
             all of the options.
 
+            ``RENDER_TYPE_CHECKBOX`` shows a checkbox beside each product.
+
         limit_per_user (Optional[int]): This restricts the number of items
             from this Category that each attendee may claim. This extends
             across multiple Invoices.
@@ -63,11 +65,13 @@ class Category(models.Model):
     RENDER_TYPE_RADIO = 1
     RENDER_TYPE_QUANTITY = 2
     RENDER_TYPE_ITEM_QUANTITY = 3
+    RENDER_TYPE_CHECKBOX = 4
 
     CATEGORY_RENDER_TYPES = [
         (RENDER_TYPE_RADIO, _("Radio button")),
         (RENDER_TYPE_QUANTITY, _("Quantity boxes")),
         (RENDER_TYPE_ITEM_QUANTITY, _("Product selector and quantity box")),
+        (RENDER_TYPE_CHECKBOX, _("Checkbox button")),
     ]
 
     name = models.CharField(
diff --git a/vendor/registrasion/registrasion/reporting/reports.py b/vendor/registrasion/registrasion/reporting/reports.py
index a9684802..d58219f9 100644
--- a/vendor/registrasion/registrasion/reporting/reports.py
+++ b/vendor/registrasion/registrasion/reporting/reports.py
@@ -177,7 +177,6 @@ class Links(Report):
         return []
 
     def rows(self, content_type):
-        print(self._links)
         for url, link_text in self._links:
             yield [
                 self._linked_text(content_type, url, link_text)
@@ -299,9 +298,10 @@ class ReportView(object):
         response = HttpResponse(content_type='text/csv')
 
         writer = csv.writer(response)
-        writer.writerow(report.headings())
+        encode = lambda i: i.encode("utf8") if isinstance(i, unicode) else i  # NOQA
+        writer.writerow(list(encode(i) for i in report.headings()))
         for row in report.rows():
-            writer.writerow(row)
+            writer.writerow(list(encode(i) for i in row))
 
         return response
 
diff --git a/vendor/registrasion/registrasion/reporting/views.py b/vendor/registrasion/registrasion/reporting/views.py
index 239c174a..fbae7906 100644
--- a/vendor/registrasion/registrasion/reporting/views.py
+++ b/vendor/registrasion/registrasion/reporting/views.py
@@ -1,4 +1,4 @@
-from registrasion.reporting import forms
+from . import forms
 
 import collections
 import datetime
@@ -24,11 +24,11 @@ from registrasion import views
 
 from symposion.schedule import models as schedule_models
 
-from registrasion.reporting.reports import get_all_reports
-from registrasion.reporting.reports import Links
-from registrasion.reporting.reports import ListReport
-from registrasion.reporting.reports import QuerysetReport
-from registrasion.reporting.reports import report_view
+from .reports import get_all_reports
+from .reports import Links
+from .reports import ListReport
+from .reports import QuerysetReport
+from .reports import report_view
 
 
 def CURRENCY():
@@ -95,8 +95,6 @@ def items_sold():
         total_quantity=Sum("quantity"),
     )
 
-    print(line_items)
-
     headings = ["Description", "Quantity", "Price", "Total"]
 
     data = []
@@ -312,6 +310,55 @@ def discount_status(request, form):
     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
+    purchashing 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")
+
+    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
@@ -353,8 +400,8 @@ def paid_invoices_by_date(request, form):
         )
         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]
+    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",
@@ -417,8 +464,6 @@ def attendee(request, form, user_id=None):
     if user_id is None:
         return attendee_list(request)
 
-    print(user_id)
-
     attendee = people.Attendee.objects.get(user__id=user_id)
     name = attendee.attendeeprofilebase.attendee_name()
 
@@ -846,9 +891,10 @@ def manifest(request, form):
     headings = ["User ID", "Name", "Paid", "Unpaid", "Refunded"]
 
     def format_items(item_list):
-        strings = []
-        for item in item_list:
-            strings.append('%d x %s' % (item.quantity, str(item.product)))
+        strings = [
+            '%d x %s' % (item.quantity, str(item.product))
+            for item in item_list
+        ]
         return ", \n".join(strings)
 
     output = []
diff --git a/vendor/registrasion/registrasion/templatetags/registrasion_tags.py b/vendor/registrasion/registrasion/templatetags/registrasion_tags.py
index 7108ae95..4c72431b 100644
--- a/vendor/registrasion/registrasion/templatetags/registrasion_tags.py
+++ b/vendor/registrasion/registrasion/templatetags/registrasion_tags.py
@@ -3,8 +3,9 @@ from registrasion.controllers.category import CategoryController
 from registrasion.controllers.item import ItemController
 
 from django import template
+from django.conf import settings
 from django.db.models import Sum
-from urllib.parse import urlencode
+from urllib import urlencode  # TODO: s/urllib/six.moves.urllib/
 
 register = template.Library()
 
@@ -117,3 +118,69 @@ def report_as_csv(context, section):
         querystring = old_query + "&" + querystring
 
     return context.request.path + "?" + querystring
+
+
+@register.assignment_tag(takes_context=True)
+def sold_out_and_unregistered(context):
+    ''' If the current user is unregistered, returns True if there are no
+    products in the TICKET_PRODUCT_CATEGORY that are available to that user.
+
+    If there *are* products available, the return False.
+
+    If the current user *is* registered, then return None (it's not a
+    pertinent question for people who already have a ticket).
+
+    '''
+
+    user = user_for_context(context)
+    if hasattr(user, "attendee") and user.attendee.completed_registration:
+        # This user has completed registration, and so we don't need to answer
+        # whether they have sold out yet.
+
+        # TODO: what if a user has got to the review phase?
+        # currently that user will hit the review page, click "Check out and
+        # pay", and that will fail. Probably good enough for now.
+
+        return None
+
+    ticket_category = settings.TICKET_PRODUCT_CATEGORY
+    categories = available_categories(context)
+
+    return ticket_category not in [cat.id for cat in categories]
+
+
+class IncludeNode(template.Node):
+    ''' https://djangosnippets.org/snippets/2058/ '''
+
+
+    def __init__(self, template_name):
+        # template_name as passed in includes quotmarks?
+        # strip them from the start and end
+        self.template_name = template_name[1:-1]
+
+    def render(self, context):
+        try:
+            # Loading the template and rendering it
+            return template.loader.render_to_string(
+                self.template_name, context=context,
+            )
+        except template.TemplateDoesNotExist:
+            return ""
+
+
+@register.tag
+def include_if_exists(parser, token):
+    """Usage: {% include_if_exists "head.html" %}
+
+    This will fail silently if the template doesn't exist. If it does, it will
+    be rendered with the current context.
+
+    From: https://djangosnippets.org/snippets/2058/
+    """
+    try:
+        tag_name, template_name = token.split_contents()
+    except ValueError:
+        raise (template.TemplateSyntaxError,
+            "%r tag requires a single argument" % token.contents.split()[0])
+
+    return IncludeNode(template_name)
diff --git a/vendor/registrasion/registrasion/tests/test_cart.py b/vendor/registrasion/registrasion/tests/test_cart.py
index d8f5b4c0..825ff675 100644
--- a/vendor/registrasion/registrasion/tests/test_cart.py
+++ b/vendor/registrasion/registrasion/tests/test_cart.py
@@ -85,7 +85,7 @@ class RegistrationCartTestCase(MixInPatches, TestCase):
             prod = inventory.Product.objects.create(
                 name="Product " + str(i + 1),
                 description="This is a test product.",
-                category=cls.categories[int(i / 2)],  # 2 products per category
+                category=cls.categories[i / 2],  # 2 products per category
                 price=Decimal("10.00"),
                 reservation_duration=cls.RESERVATION,
                 limit_per_user=10,
diff --git a/vendor/registrasion/registrasion/tests/test_invoice.py b/vendor/registrasion/registrasion/tests/test_invoice.py
index 0c650dd4..b77fa223 100644
--- a/vendor/registrasion/registrasion/tests/test_invoice.py
+++ b/vendor/registrasion/registrasion/tests/test_invoice.py
@@ -98,11 +98,7 @@ class InvoiceTestCase(TestHelperMixin, RegistrationCartTestCase):
 
     def test_total_payments_balance_due(self):
         invoice = self._invoice_containing_prod_1(2)
-        # range only takes int, and the following logic fails if not a round
-        # number.  So fail if we are not a round number so developer may fix
-        # this test or the product.
-        self.assertTrue((invoice.invoice.value % 1).is_zero())
-        for i in range(0, int(invoice.invoice.value)):
+        for i in xrange(0, invoice.invoice.value):
             self.assertTrue(
                 i + 1, invoice.invoice.total_payments()
             )
diff --git a/vendor/registrasion/registrasion/tests/test_speaker.py b/vendor/registrasion/registrasion/tests/test_speaker.py
index f31266b6..cf64074e 100644
--- a/vendor/registrasion/registrasion/tests/test_speaker.py
+++ b/vendor/registrasion/registrasion/tests/test_speaker.py
@@ -67,7 +67,7 @@ class SpeakerTestCase(RegistrationCartTestCase):
             kind=kind_1,
             title="Proposal 1",
             abstract="Abstract",
-            private_abstract="Private Abstract",
+            description="Description",
             speaker=speaker_1,
         )
         proposal_models.AdditionalSpeaker.objects.create(
@@ -80,7 +80,7 @@ class SpeakerTestCase(RegistrationCartTestCase):
             kind=kind_2,
             title="Proposal 2",
             abstract="Abstract",
-            private_abstract="Private Abstract",
+            description="Description",
             speaker=speaker_1,
         )
         proposal_models.AdditionalSpeaker.objects.create(
diff --git a/vendor/registrasion/registrasion/urls.py b/vendor/registrasion/registrasion/urls.py
index 0789912d..6afdd5f0 100644
--- a/vendor/registrasion/registrasion/urls.py
+++ b/vendor/registrasion/registrasion/urls.py
@@ -1,4 +1,4 @@
-from registrasion.reporting import views as rv
+from .reporting import views as rv
 
 from django.conf.urls import include
 from django.conf.urls import url
@@ -19,6 +19,7 @@ from .views import (
     product_category,
     refund,
     review,
+    voucher_code,
 )
 
 
@@ -43,6 +44,7 @@ public = [
     url(r"^profile$", edit_profile, name="attendee_edit"),
     url(r"^register$", guided_registration, name="guided_registration"),
     url(r"^review$", review, name="review"),
+    url(r"^voucher$", voucher_code, name="voucher_code"),
     url(r"^register/([0-9]+)$", guided_registration,
         name="guided_registration"),
 ]
@@ -55,6 +57,11 @@ reports = [
     url(r"^attendee/([0-9]*)$", rv.attendee, name="attendee"),
     url(r"^credit_notes/?$", rv.credit_notes, name="credit_notes"),
     url(r"^manifest/?$", rv.manifest, name="manifest"),
+    url(
+        r"^product_line_items/?$",
+        rv.product_line_items,
+        name="product_line_items",
+    ),
     url(r"^discount_status/?$", rv.discount_status, name="discount_status"),
     url(r"^invoices/?$", rv.invoices, name="invoices"),
     url(
diff --git a/vendor/registrasion/registrasion/util.py b/vendor/registrasion/registrasion/util.py
index 98079c7e..b5fa0620 100644
--- a/vendor/registrasion/registrasion/util.py
+++ b/vendor/registrasion/registrasion/util.py
@@ -12,7 +12,7 @@ def generate_access_code():
 
     length = 6
     # all upper-case letters + digits 1-9 (no 0 vs O confusion)
-    chars = string.ascii_uppercase + string.digits[1:]
+    chars = string.uppercase + string.digits[1:]
     # 6 chars => 35 ** 6 = 1838265625 (should be enough for anyone)
     return get_random_string(length=length, allowed_chars=chars)
 
diff --git a/vendor/registrasion/registrasion/views.py b/vendor/registrasion/registrasion/views.py
index 239e924b..e16313f2 100644
--- a/vendor/registrasion/registrasion/views.py
+++ b/vendor/registrasion/registrasion/views.py
@@ -1,19 +1,20 @@
 import datetime
 import zipfile
 
-from registrasion import forms
-from registrasion import util
-from registrasion.models import commerce
-from registrasion.models import inventory
-from registrasion.models import people
-from registrasion.controllers.batch import BatchController
-from registrasion.controllers.cart import CartController
-from registrasion.controllers.credit_note import CreditNoteController
-from registrasion.controllers.discount import DiscountController
-from registrasion.controllers.invoice import InvoiceController
-from registrasion.controllers.item import ItemController
-from registrasion.controllers.product import ProductController
-from registrasion.exceptions import CartValidationError
+from . import forms
+from . import util
+from .models import commerce
+from .models import inventory
+from .models import people
+from .controllers.batch import BatchController
+from .controllers.cart import CartController
+from .controllers.category import CategoryController
+from .controllers.credit_note import CreditNoteController
+from .controllers.discount import DiscountController
+from .controllers.invoice import InvoiceController
+from .controllers.item import ItemController
+from .controllers.product import ProductController
+from .exceptions import CartValidationError
 
 from collections import namedtuple
 
@@ -64,12 +65,19 @@ class GuidedRegistrationSection(_GuidedRegistrationSection):
 
 
 @login_required
-def guided_registration(request):
+def guided_registration(request, page_number=None):
     ''' Goes through the registration process in order, making sure user sees
     all valid categories.
 
     The user must be logged in to see this view.
 
+    Parameter:
+        page_number:
+            1) Profile form (and e-mail address?)
+            2) Ticket type
+            3) Remaining products
+            4) Mark registration as complete
+
     Returns:
         render: Renders ``registrasion/guided_registration.html``,
             with the following data::
@@ -85,148 +93,225 @@ def guided_registration(request):
 
     '''
 
-    SESSION_KEY = "guided_registration_categories"
-    ASK_FOR_PROFILE = 777  # Magic number. Meh.
+    PAGE_PROFILE = 1
+    PAGE_TICKET = 2
+    PAGE_PRODUCTS = 3
+    PAGE_PRODUCTS_MAX = 4
+    TOTAL_PAGES = 4
 
-    next_step = redirect("guided_registration")
-
-    sections = []
+    ticket_category = inventory.Category.objects.get(
+        id=settings.TICKET_PRODUCT_CATEGORY
+    )
+    cart = CartController.for_user(request.user)
 
     attendee = people.Attendee.get_instance(request.user)
 
+    # This guided registration process is only for people who have
+    # not completed registration (and has confusing behaviour if you go
+    # back to it.)
     if attendee.completed_registration:
         return redirect(review)
 
-    # Step 1: Fill in a badge and collect a voucher code
-    try:
-        profile = attendee.attendeeprofilebase
-    except ObjectDoesNotExist:
-        profile = None
-
-    # Figure out if we need to show the profile form and the voucher form
-    show_profile_and_voucher = False
-    if SESSION_KEY not in request.session:
-        if not profile:
-            show_profile_and_voucher = True
+    # Calculate the current maximum page number for this user.
+    has_profile = hasattr(attendee, "attendeeprofilebase")
+    if not has_profile:
+        # If there's no profile, they have to go to the profile page.
+        max_page = PAGE_PROFILE
+        redirect_page = PAGE_PROFILE
     else:
-        if request.session[SESSION_KEY] == ASK_FOR_PROFILE:
-            show_profile_and_voucher = True
-
-    if show_profile_and_voucher:
-        # Keep asking for the profile until everything passes.
-        request.session[SESSION_KEY] = ASK_FOR_PROFILE
-
-        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,
+        # We have a profile.
+        # Do they have a ticket?
+        products = inventory.Product.objects.filter(
+            productitem__cart=cart.cart
         )
+        products = products.filter(category=ticket_category)
 
-        profile_section = GuidedRegistrationSection(
-            title="Profile and Personal Information",
-            form=profile_form,
-        )
-
-        title = "Attendee information"
-        current_step = 1
-        sections.append(voucher_section)
-        sections.append(profile_section)
-    else:
-        # We're selling products
-
-        starting = attendee.guided_categories_complete.count() == 0
-
-        # Get the next category
-        cats = inventory.Category.objects
-        if SESSION_KEY in request.session:
-            _cats = request.session[SESSION_KEY]
-            cats = cats.filter(id__in=_cats)
+        if products.count() == 0:
+            # If no ticket, they can only see the profile or ticket page.
+            max_page = PAGE_TICKET
+            redirect_page = PAGE_TICKET
         else:
-            cats = cats.exclude(
-                id__in=attendee.guided_categories_complete.all(),
+            # If there's a ticket, they should *see* the general products page#
+            # but be able to go to the overflow page if needs be.
+            max_page = PAGE_PRODUCTS_MAX
+            redirect_page = PAGE_PRODUCTS
+
+    if page_number is None or int(page_number) > max_page:
+        return redirect("guided_registration", redirect_page)
+
+    page_number = int(page_number)
+
+    next_step = redirect("guided_registration", page_number + 1)
+
+    with BatchController.batch(request.user):
+
+        # This view doesn't work if the conference has sold out.
+        available = ProductController.available_products(
+            request.user, category=ticket_category
+        )
+        if not available:
+            messages.error(request, "There are no more tickets available.")
+            return redirect("dashboard")
+
+        sections = []
+
+        # Build up the list of sections
+        if page_number == PAGE_PROFILE:
+            # Profile bit
+            title = "Attendee information"
+            sections = _guided_registration_profile_and_voucher(request)
+        elif page_number == PAGE_TICKET:
+            # Select ticket
+            title = "Select ticket type"
+            sections = _guided_registration_products(
+                request, GUIDED_MODE_TICKETS_ONLY
+            )
+        elif page_number == PAGE_PRODUCTS:
+            # Select additional items
+            title = "Additional items"
+            sections = _guided_registration_products(
+                request, GUIDED_MODE_ALL_ADDITIONAL
+            )
+        elif page_number == PAGE_PRODUCTS_MAX:
+            # Items enabled by things on page 3 -- only shows things
+            # that have not been marked as complete.
+            title = "More additional items"
+            sections = _guided_registration_products(
+                request, GUIDED_MODE_EXCLUDE_COMPLETE
             )
 
-        cats = cats.order_by("order")
+        if not sections:
+            # We've filled in every category
+            attendee.completed_registration = True
+            attendee.save()
+            return redirect("review")
 
-        request.session[SESSION_KEY] = []
-
-        if starting:
-            # Only display the first Category
-            title = "Select ticket type"
-            current_step = 2
-            cats = [cats[0]]
-        else:
-            # Set title appropriately for remaining categories
-            current_step = 3
-            title = "Additional items"
-
-        all_products = inventory.Product.objects.filter(
-            category__in=cats,
-        ).select_related("category")
-
-        with BatchController.batch(request.user):
-            available_products = set(ProductController.available_products(
-                request.user,
-                products=all_products,
-            ))
-
-            if len(available_products) == 0:
-                # We've filled in every category
-                attendee.completed_registration = True
-                attendee.save()
+        if sections and request.method == "POST":
+            for section in sections:
+                if section.form.errors:
+                    break
+            else:
+                # We've successfully processed everything
                 return next_step
 
-            for category in cats:
-                products = [
-                    i for i in available_products
-                    if i.category == category
-                ]
-
-                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)
-                    # Add this to the list of things to show if the form
-                    # errors.
-                    request.session[SESSION_KEY].append(category.id)
-
-                    if request.method == "POST" and not products_form.errors:
-                        # This is only saved if we pass each form with no
-                        # errors, and if the form actually has products.
-                        attendee.guided_categories_complete.add(category)
-
-    if sections and request.method == "POST":
-        for section in sections:
-            if section.form.errors:
-                break
-        else:
-            attendee.save()
-            if SESSION_KEY in request.session:
-                del request.session[SESSION_KEY]
-            # We've successfully processed everything
-            return next_step
-
     data = {
-        "current_step": current_step,
+        "current_step": page_number,
         "sections": sections,
         "title": title,
-        "total_steps": 3,
+        "total_steps": TOTAL_PAGES,
     }
     return render(request, "registrasion/guided_registration.html", data)
 
 
+GUIDED_MODE_TICKETS_ONLY = 2
+GUIDED_MODE_ALL_ADDITIONAL = 3
+GUIDED_MODE_EXCLUDE_COMPLETE = 4
+
+
+@login_required
+def _guided_registration_products(request, mode):
+    sections = []
+
+    SESSION_KEY = "guided_registration"
+    MODE_KEY = "mode"
+    CATS_KEY = "cats"
+
+    attendee = people.Attendee.get_instance(request.user)
+
+    # Get the next category
+    cats = inventory.Category.objects.order_by("order")  # TODO: default order?
+
+    # Fun story: If _any_ of the category forms result in an error, but other
+    # new products get enabled with a flag, those new products will appear.
+    # We need to make sure that we only display the products that were valid
+    # in the first place. So we track them in a session, and refresh only if
+    # the page number does not change. Cheap!
+
+    if SESSION_KEY in request.session:
+        session_struct = request.session[SESSION_KEY]
+        old_mode = session_struct[MODE_KEY]
+        old_cats = session_struct[CATS_KEY]
+    else:
+        old_mode = None
+        old_cats = []
+
+    if mode == old_mode:
+        cats = cats.filter(id__in=old_cats)
+    elif mode == GUIDED_MODE_TICKETS_ONLY:
+        cats = cats.filter(id=settings.TICKET_PRODUCT_CATEGORY)
+    elif mode == GUIDED_MODE_ALL_ADDITIONAL:
+        cats = cats.exclude(id=settings.TICKET_PRODUCT_CATEGORY)
+    elif mode == GUIDED_MODE_EXCLUDE_COMPLETE:
+        cats = cats.exclude(id=settings.TICKET_PRODUCT_CATEGORY)
+        cats = cats.exclude(id__in=old_cats)
+
+    # We update the session key at the end of this method
+    # once we've found all the categories that have available products
+
+    all_products = inventory.Product.objects.filter(
+        category__in=cats,
+    ).select_related("category")
+
+    seen_categories = []
+
+    with BatchController.batch(request.user):
+        available_products = set(ProductController.available_products(
+            request.user,
+            products=all_products,
+        ))
+
+        if len(available_products) == 0:
+            return []
+
+        has_errors = False
+
+        for category in cats:
+            products = [
+                i for i in available_products
+                if i.category == category
+            ]
+
+            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,
+    )
+
+    return [voucher_section, profile_section]
+
+
 @login_required
 def review(request):
     ''' View for the review page. '''
@@ -399,6 +484,28 @@ def product_category(request, category_id):
     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