Merge branch 'documentation'
This commit is contained in:
commit
203a4a2212
11 changed files with 373 additions and 62 deletions
|
@ -1,4 +1,3 @@
|
||||||
================================
|
|
||||||
Registrasion for Zookeepr Keeprs
|
Registrasion for Zookeepr Keeprs
|
||||||
================================
|
================================
|
||||||
|
|
||||||
|
@ -16,11 +15,8 @@ Things that are the same
|
||||||
|
|
||||||
Things that are different
|
Things that are different
|
||||||
-------------------------
|
-------------------------
|
||||||
* Ceilings can be used to apply discounts, so Early Bird ticket rates can be
|
* Ceilings can be used to apply discounts, so Early Bird ticket rates can be implemented by applying a ceiling-type discount, rather than duplicating the ticket type.
|
||||||
implemented by applying a ceiling-type discount, rather than duplicating the
|
|
||||||
ticket type.
|
|
||||||
* Vouchers can be used to enable products
|
* Vouchers can be used to enable products
|
||||||
* e.g. Sponsor tickets do not appear until you supply a sponsor's voucher
|
* e.g. Sponsor tickets do not appear until you supply a sponsor's voucher
|
||||||
* Items may be enabled by having other specific items present
|
* Items may be enabled by having other specific items present
|
||||||
* e.g. Extra accommodation nights do not appear until you purchase the main
|
* e.g. Extra accommodation nights do not appear until you purchase the main week worth of accommodation.
|
||||||
week worth of accommodation.
|
|
||||||
|
|
|
@ -19,14 +19,17 @@ Contents:
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
overview
|
overview
|
||||||
|
integration
|
||||||
inventory
|
inventory
|
||||||
payments
|
payments
|
||||||
for-zookeepr-users
|
for-zookeepr-users
|
||||||
|
views
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
.. * :ref:`modindex`
|
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
|
|
||||||
|
.. * :ref:`modindex`
|
||||||
|
|
37
docs/integration.rst
Normal file
37
docs/integration.rst
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
Integrating Registrasion
|
||||||
|
========================
|
||||||
|
|
||||||
|
Registrasion is a Django app. It does not provide any templates -- you'll need to develop these yourself. You can use the ``registrasion-demo`` project as a starting point.
|
||||||
|
|
||||||
|
To use Registrasion for your own conference, you'll need to do a small amount of development work, usually in your own Django App.
|
||||||
|
|
||||||
|
The first is to define a model and form for your attendee profile, and the second is to implement a payment app.
|
||||||
|
|
||||||
|
|
||||||
|
Attendee profile
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. automodule:: registrasion.models.people
|
||||||
|
|
||||||
|
Attendee profiles are where you ask for information such as what your attendee wants on their badge, and what the attendee's dietary and accessibility requirements are.
|
||||||
|
|
||||||
|
Because every conference is different, Registrasion lets you define your own attendee profile model, and your own form for requesting this information. The only requirement is that you derive your model from ``AttendeeProfileBase``.
|
||||||
|
|
||||||
|
.. autoclass :: AttendeeProfileBase
|
||||||
|
:members: name_field, invoice_recipient
|
||||||
|
|
||||||
|
Once you've subclassed ``AttendeeProfileBase``, you'll need to implement a form that lets attendees fill out their profile.
|
||||||
|
|
||||||
|
You specify how to find that form in your Django ``settings.py`` file::
|
||||||
|
|
||||||
|
ATTENDEE_PROFILE_FORM = "democon.forms.AttendeeProfileForm"
|
||||||
|
|
||||||
|
The only contract is that this form creates an instance of ``AttendeeProfileBase`` when saved, and that it can take an instance of your subclass on creation (so that your attendees can edit their profile).
|
||||||
|
|
||||||
|
|
||||||
|
Payments
|
||||||
|
--------
|
||||||
|
|
||||||
|
Registrasion does not implement its own credit card processing. You'll need to do that yourself. Registrasion *does* provide a mechanism for recording cheques and direct deposits, if you do end up taking registrations that way.
|
||||||
|
|
||||||
|
See :ref:`payments_and_refunds` for a guide on how to correctly implement payments.
|
|
@ -1,4 +1,5 @@
|
||||||
.. automodule:: registrasion.models.commerce
|
.. automodule:: registrasion.models.commerce
|
||||||
|
.. _payments_and_refunds:
|
||||||
|
|
||||||
Payments and Refunds
|
Payments and Refunds
|
||||||
====================
|
====================
|
||||||
|
@ -140,7 +141,7 @@ Credits can be applied to invoices::
|
||||||
This will result in an instance of ``CreditNoteApplication`` being applied as a payment to ``invoice``. ``CreditNoteApplication`` will always be a payment of the full amount of its parent credit note. If this payment overpays the invoice it's being applied to, a credit note for the residual will be generated.
|
This will result in an instance of ``CreditNoteApplication`` being applied as a payment to ``invoice``. ``CreditNoteApplication`` will always be a payment of the full amount of its parent credit note. If this payment overpays the invoice it's being applied to, a credit note for the residual will be generated.
|
||||||
|
|
||||||
Refunding credit notes
|
Refunding credit notes
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
It is possible to release a credit note back to the original payment platform. To do so, you attach an instance of ``CreditNoteRefund`` to the original ``CreditNote``:
|
It is possible to release a credit note back to the original payment platform. To do so, you attach an instance of ``CreditNoteRefund`` to the original ``CreditNote``:
|
||||||
|
|
||||||
.. autoclass :: CreditNoteRefund
|
.. autoclass :: CreditNoteRefund
|
||||||
|
|
16
docs/views.rst
Normal file
16
docs/views.rst
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
Public-facing views
|
||||||
|
===================
|
||||||
|
|
||||||
|
Here's all of the views that Registrasion exposes to the public.
|
||||||
|
|
||||||
|
.. automodule:: registrasion.views
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Template tags
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Registrasion makes template tags available:
|
||||||
|
|
||||||
|
.. automodule:: registrasion.templatetags.registrasion_tags
|
||||||
|
:members:
|
|
@ -311,7 +311,7 @@ class CartController(object):
|
||||||
commerce.DiscountItem.objects.filter(cart=self.cart).delete()
|
commerce.DiscountItem.objects.filter(cart=self.cart).delete()
|
||||||
|
|
||||||
product_items = self.cart.productitem_set.all().select_related(
|
product_items = self.cart.productitem_set.all().select_related(
|
||||||
"product", "product__category",
|
"product", "product__category", "product__price"
|
||||||
)
|
)
|
||||||
|
|
||||||
products = [i.product for i in product_items]
|
products = [i.product for i in product_items]
|
||||||
|
@ -319,9 +319,6 @@ class CartController(object):
|
||||||
|
|
||||||
# The highest-value discounts will apply to the highest-value
|
# The highest-value discounts will apply to the highest-value
|
||||||
# products first.
|
# products first.
|
||||||
product_items = self.cart.productitem_set.all()
|
|
||||||
product_items = product_items.select_related("product")
|
|
||||||
product_items = product_items.order_by('product__price')
|
|
||||||
product_items = reversed(product_items)
|
product_items = reversed(product_items)
|
||||||
for item in product_items:
|
for item in product_items:
|
||||||
self._add_discount(item.product, item.quantity, discounts)
|
self._add_discount(item.product, item.quantity, discounts)
|
||||||
|
|
|
@ -392,8 +392,8 @@ class ProductFlag(EnablingConditionBase):
|
||||||
''' The condition is met because a specific product is purchased.
|
''' The condition is met because a specific product is purchased.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
enabling_products ([inventory.Product, ...]): The products that cause this
|
enabling_products ([inventory.Product, ...]): The products that cause
|
||||||
condition to be met.
|
this condition to be met.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -59,13 +59,22 @@ class AttendeeProfileBase(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def name_field(cls):
|
def name_field(cls):
|
||||||
''' This is used to pre-fill the attendee's name from the
|
'''
|
||||||
speaker profile. If it's None, that functionality is disabled. '''
|
Returns:
|
||||||
|
The name of a field that stores the attendee's name. This is used
|
||||||
|
to pre-fill the attendee's name from their Speaker profile, if they
|
||||||
|
have one.
|
||||||
|
'''
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def invoice_recipient(self):
|
def invoice_recipient(self):
|
||||||
''' Returns a representation of this attendee profile for the purpose
|
'''
|
||||||
of rendering to an invoice. Override in subclasses. '''
|
|
||||||
|
Returns:
|
||||||
|
A representation of this attendee profile for the purpose
|
||||||
|
of rendering to an invoice. This should include any information
|
||||||
|
that you'd usually include on an invoice. Override in subclasses.
|
||||||
|
'''
|
||||||
|
|
||||||
# Manual dispatch to subclass. Fleh.
|
# Manual dispatch to subclass. Fleh.
|
||||||
slf = AttendeeProfileBase.objects.get_subclass(id=self.id)
|
slf = AttendeeProfileBase.objects.get_subclass(id=self.id)
|
||||||
|
|
|
@ -8,18 +8,42 @@ from django.db.models import Sum
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
ProductAndQuantity = namedtuple("ProductAndQuantity", ["product", "quantity"])
|
_ProductAndQuantity = namedtuple("ProductAndQuantity", ["product", "quantity"])
|
||||||
|
|
||||||
|
class ProductAndQuantity(_ProductAndQuantity):
|
||||||
|
''' Class that holds a product and a quantity.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
product (models.inventory.Product)
|
||||||
|
|
||||||
|
quantity (int)
|
||||||
|
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def available_categories(context):
|
def available_categories(context):
|
||||||
''' Returns all of the available product categories '''
|
''' Gets all of the currently available products.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[models.inventory.Category, ...]: A list of all of the categories that
|
||||||
|
have Products that the current user can reserve.
|
||||||
|
|
||||||
|
'''
|
||||||
return CategoryController.available_categories(context.request.user)
|
return CategoryController.available_categories(context.request.user)
|
||||||
|
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def available_credit(context):
|
def available_credit(context):
|
||||||
''' Returns the amount of unclaimed credit available for this user. '''
|
''' Calculates the sum of unclaimed credit from this user's credit notes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: the sum of the values of unclaimed credit notes for the
|
||||||
|
current user.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
notes = commerce.CreditNote.unclaimed().filter(
|
notes = commerce.CreditNote.unclaimed().filter(
|
||||||
invoice__user=context.request.user,
|
invoice__user=context.request.user,
|
||||||
)
|
)
|
||||||
|
@ -29,14 +53,23 @@ def available_credit(context):
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def invoices(context):
|
def invoices(context):
|
||||||
''' Returns all of the invoices that this user has. '''
|
'''
|
||||||
return commerce.Invoice.objects.filter(cart__user=context.request.user)
|
|
||||||
|
Returns:
|
||||||
|
[models.commerce.Invoice, ...]: All of the current user's invoices. '''
|
||||||
|
return commerce.Invoice.objects.filter(user=context.request.user)
|
||||||
|
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def items_pending(context):
|
def items_pending(context):
|
||||||
''' Returns all of the items that this user has in their current cart,
|
''' Gets all of the items that the user has reserved, but has not yet
|
||||||
and is awaiting payment. '''
|
paid for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[ProductAndQuantity, ...]: A list of product-quantity pairs for the
|
||||||
|
items that the user has not yet paid for.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
all_items = commerce.ProductItem.objects.filter(
|
all_items = commerce.ProductItem.objects.filter(
|
||||||
cart__user=context.request.user,
|
cart__user=context.request.user,
|
||||||
|
@ -53,8 +86,17 @@ def items_pending(context):
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def items_purchased(context, category=None):
|
def items_purchased(context, category=None):
|
||||||
''' Returns all of the items that this user has purchased, optionally
|
''' Aggregates the items that this user has purchased.
|
||||||
from the given category. '''
|
|
||||||
|
Arguments:
|
||||||
|
category (Optional[models.inventory.Category]): the category of items
|
||||||
|
to restrict to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[ProductAndQuantity, ...]: A list of product-quantity pairs,
|
||||||
|
aggregating like products from across multiple invoices.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
all_items = commerce.ProductItem.objects.filter(
|
all_items = commerce.ProductItem.objects.filter(
|
||||||
cart__user=context.request.user,
|
cart__user=context.request.user,
|
||||||
|
@ -76,5 +118,20 @@ def items_purchased(context, category=None):
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def multiply(value, arg):
|
def multiply(value, arg):
|
||||||
''' Multiplies value by arg '''
|
''' Multiplies value by arg.
|
||||||
|
|
||||||
|
This is useful when displaying invoices, as it lets you multiply the
|
||||||
|
quantity by the unit value.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
value (number)
|
||||||
|
|
||||||
|
arg (number)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
number: value * arg
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
return value * arg
|
return value * arg
|
||||||
|
|
|
@ -14,3 +14,14 @@ def generate_access_code():
|
||||||
chars = string.uppercase + string.digits[1:]
|
chars = string.uppercase + string.digits[1:]
|
||||||
# 4 chars => 35 ** 4 = 1500625 (should be enough for anyone)
|
# 4 chars => 35 ** 4 = 1500625 (should be enough for anyone)
|
||||||
return get_random_string(length=length, allowed_chars=chars)
|
return get_random_string(length=length, allowed_chars=chars)
|
||||||
|
|
||||||
|
|
||||||
|
def all_arguments_optional(ntcls):
|
||||||
|
''' Takes a namedtuple derivative and makes all of the arguments optional.
|
||||||
|
'''
|
||||||
|
|
||||||
|
ntcls.__new__.__defaults__ = (
|
||||||
|
(None,) * len(ntcls._fields)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ntcls
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from registrasion import forms
|
from registrasion import forms
|
||||||
|
from registrasion import util
|
||||||
from registrasion.models import commerce
|
from registrasion.models import commerce
|
||||||
from registrasion.models import inventory
|
from registrasion.models import inventory
|
||||||
from registrasion.models import people
|
from registrasion.models import people
|
||||||
|
@ -24,7 +25,7 @@ from django.shortcuts import redirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
GuidedRegistrationSection = namedtuple(
|
_GuidedRegistrationSection = namedtuple(
|
||||||
"GuidedRegistrationSection",
|
"GuidedRegistrationSection",
|
||||||
(
|
(
|
||||||
"title",
|
"title",
|
||||||
|
@ -33,9 +34,26 @@ GuidedRegistrationSection = namedtuple(
|
||||||
"form",
|
"form",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
GuidedRegistrationSection.__new__.__defaults__ = (
|
|
||||||
(None,) * len(GuidedRegistrationSection._fields)
|
|
||||||
)
|
@util.all_arguments_optional
|
||||||
|
class GuidedRegistrationSection(_GuidedRegistrationSection):
|
||||||
|
''' Represents a section of a guided registration page.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
title (str): The title of the section.
|
||||||
|
|
||||||
|
discounts ([registrasion.contollers.discount.DiscountAndQuantity, ...]):
|
||||||
|
A list of discount objects that are available in the section. You
|
||||||
|
can display ``.clause`` to show what the discount applies to, and
|
||||||
|
``.quantity`` to display the number of times that discount can be
|
||||||
|
applied.
|
||||||
|
|
||||||
|
description (str): A description of the section.
|
||||||
|
|
||||||
|
form (forms.Form): A form to display.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_form(name):
|
def get_form(name):
|
||||||
|
@ -46,13 +64,25 @@ def get_form(name):
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def guided_registration(request, page_id=0):
|
def guided_registration(request):
|
||||||
''' Goes through the registration process in order,
|
''' Goes through the registration process in order, making sure user sees
|
||||||
making sure user sees all valid categories.
|
all valid categories.
|
||||||
|
|
||||||
|
The user must be logged in to see this view.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
render: Renders ``registrasion/guided_registration.html``,
|
||||||
|
with the following data::
|
||||||
|
|
||||||
|
{
|
||||||
|
"current_step": int(), # The current step in the
|
||||||
|
# registration
|
||||||
|
"sections": sections, # A list of
|
||||||
|
# GuidedRegistrationSections
|
||||||
|
"title": str(), # The title of the page
|
||||||
|
"total_steps": int(), # The total number of steps
|
||||||
|
}
|
||||||
|
|
||||||
WORK IN PROGRESS: the finalised version of this view will allow
|
|
||||||
grouping of categories into a specific page. Currently, it just goes
|
|
||||||
through each category one by one
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
SESSION_KEY = "guided_registration_categories"
|
SESSION_KEY = "guided_registration_categories"
|
||||||
|
@ -90,8 +120,8 @@ def guided_registration(request, page_id=0):
|
||||||
# Keep asking for the profile until everything passes.
|
# Keep asking for the profile until everything passes.
|
||||||
request.session[SESSION_KEY] = ASK_FOR_PROFILE
|
request.session[SESSION_KEY] = ASK_FOR_PROFILE
|
||||||
|
|
||||||
voucher_form, voucher_handled = handle_voucher(request, "voucher")
|
voucher_form, voucher_handled = _handle_voucher(request, "voucher")
|
||||||
profile_form, profile_handled = handle_profile(request, "profile")
|
profile_form, profile_handled = _handle_profile(request, "profile")
|
||||||
|
|
||||||
voucher_section = GuidedRegistrationSection(
|
voucher_section = GuidedRegistrationSection(
|
||||||
title="Voucher Code",
|
title="Voucher Code",
|
||||||
|
@ -158,7 +188,7 @@ def guided_registration(request, page_id=0):
|
||||||
]
|
]
|
||||||
|
|
||||||
prefix = "category_" + str(category.id)
|
prefix = "category_" + str(category.id)
|
||||||
p = handle_products(request, category, products, prefix)
|
p = _handle_products(request, category, products, prefix)
|
||||||
products_form, discounts, products_handled = p
|
products_form, discounts, products_handled = p
|
||||||
|
|
||||||
section = GuidedRegistrationSection(
|
section = GuidedRegistrationSection(
|
||||||
|
@ -201,7 +231,23 @@ def guided_registration(request, page_id=0):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def edit_profile(request):
|
def edit_profile(request):
|
||||||
form, handled = handle_profile(request, "profile")
|
''' 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
form, handled = _handle_profile(request, "profile")
|
||||||
|
|
||||||
if handled and not form.errors:
|
if handled and not form.errors:
|
||||||
messages.success(
|
messages.success(
|
||||||
|
@ -216,7 +262,7 @@ def edit_profile(request):
|
||||||
return render(request, "registrasion/profile_form.html", data)
|
return render(request, "registrasion/profile_form.html", data)
|
||||||
|
|
||||||
|
|
||||||
def handle_profile(request, prefix):
|
def _handle_profile(request, prefix):
|
||||||
''' Returns a profile form instance, and a boolean which is true if the
|
''' Returns a profile form instance, and a boolean which is true if the
|
||||||
form was handled. '''
|
form was handled. '''
|
||||||
attendee = people.Attendee.get_instance(request.user)
|
attendee = people.Attendee.get_instance(request.user)
|
||||||
|
@ -262,7 +308,28 @@ def handle_profile(request, prefix):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def product_category(request, category_id):
|
def product_category(request, category_id):
|
||||||
''' Registration selections form for a specific category of items.
|
''' 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"
|
PRODUCTS_FORM_PREFIX = "products"
|
||||||
|
@ -270,7 +337,7 @@ def product_category(request, category_id):
|
||||||
|
|
||||||
# Handle the voucher form *before* listing products.
|
# Handle the voucher form *before* listing products.
|
||||||
# Products can change as vouchers are entered.
|
# Products can change as vouchers are entered.
|
||||||
v = handle_voucher(request, VOUCHERS_FORM_PREFIX)
|
v = _handle_voucher(request, VOUCHERS_FORM_PREFIX)
|
||||||
voucher_form, voucher_handled = v
|
voucher_form, voucher_handled = v
|
||||||
|
|
||||||
category_id = int(category_id) # Routing is [0-9]+
|
category_id = int(category_id) # Routing is [0-9]+
|
||||||
|
@ -288,7 +355,7 @@ def product_category(request, category_id):
|
||||||
)
|
)
|
||||||
return redirect("dashboard")
|
return redirect("dashboard")
|
||||||
|
|
||||||
p = handle_products(request, category, products, PRODUCTS_FORM_PREFIX)
|
p = _handle_products(request, category, products, PRODUCTS_FORM_PREFIX)
|
||||||
products_form, discounts, products_handled = p
|
products_form, discounts, products_handled = p
|
||||||
|
|
||||||
if request.POST and not voucher_handled and not products_form.errors:
|
if request.POST and not voucher_handled and not products_form.errors:
|
||||||
|
@ -310,7 +377,7 @@ def product_category(request, category_id):
|
||||||
return render(request, "registrasion/product_category.html", data)
|
return render(request, "registrasion/product_category.html", data)
|
||||||
|
|
||||||
|
|
||||||
def handle_products(request, category, products, prefix):
|
def _handle_products(request, category, products, prefix):
|
||||||
''' Handles a products list form in the given request. Returns the
|
''' Handles a products list form in the given request. Returns the
|
||||||
form instance, the discounts applicable to this form, and whether the
|
form instance, the discounts applicable to this form, and whether the
|
||||||
contents were handled. '''
|
contents were handled. '''
|
||||||
|
@ -343,7 +410,7 @@ def handle_products(request, category, products, prefix):
|
||||||
|
|
||||||
if request.method == "POST" and products_form.is_valid():
|
if request.method == "POST" and products_form.is_valid():
|
||||||
if products_form.has_changed():
|
if products_form.has_changed():
|
||||||
set_quantities_from_products_form(products_form, current_cart)
|
_set_quantities_from_products_form(products_form, current_cart)
|
||||||
|
|
||||||
# If category is required, the user must have at least one
|
# If category is required, the user must have at least one
|
||||||
# in an active+valid cart
|
# in an active+valid cart
|
||||||
|
@ -365,7 +432,7 @@ def handle_products(request, category, products, prefix):
|
||||||
return products_form, discounts, handled
|
return products_form, discounts, handled
|
||||||
|
|
||||||
|
|
||||||
def set_quantities_from_products_form(products_form, current_cart):
|
def _set_quantities_from_products_form(products_form, current_cart):
|
||||||
|
|
||||||
quantities = list(products_form.product_quantities())
|
quantities = list(products_form.product_quantities())
|
||||||
|
|
||||||
|
@ -395,7 +462,7 @@ def set_quantities_from_products_form(products_form, current_cart):
|
||||||
products_form.add_error(field, message)
|
products_form.add_error(field, message)
|
||||||
|
|
||||||
|
|
||||||
def handle_voucher(request, prefix):
|
def _handle_voucher(request, prefix):
|
||||||
''' Handles a voucher form in the given request. Returns the voucher
|
''' Handles a voucher form in the given request. Returns the voucher
|
||||||
form instance, and whether the voucher code was handled. '''
|
form instance, and whether the voucher code was handled. '''
|
||||||
|
|
||||||
|
@ -426,8 +493,25 @@ def handle_voucher(request, prefix):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def checkout(request):
|
def checkout(request):
|
||||||
''' Runs checkout for the current cart of items, ideally generating an
|
''' Runs the checkout process for the current cart.
|
||||||
invoice. '''
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
current_cart = CartController.for_user(request.user)
|
current_cart = CartController.for_user(request.user)
|
||||||
|
|
||||||
|
@ -437,12 +521,12 @@ def checkout(request):
|
||||||
try:
|
try:
|
||||||
current_invoice = InvoiceController.for_cart(current_cart.cart)
|
current_invoice = InvoiceController.for_cart(current_cart.cart)
|
||||||
except ValidationError as ve:
|
except ValidationError as ve:
|
||||||
return checkout_errors(request, ve)
|
return _checkout_errors(request, ve)
|
||||||
|
|
||||||
return redirect("invoice", current_invoice.invoice.id)
|
return redirect("invoice", current_invoice.invoice.id)
|
||||||
|
|
||||||
|
|
||||||
def checkout_errors(request, errors):
|
def _checkout_errors(request, errors):
|
||||||
|
|
||||||
error_list = []
|
error_list = []
|
||||||
for error in errors.error_list:
|
for error in errors.error_list:
|
||||||
|
@ -459,7 +543,20 @@ def checkout_errors(request, errors):
|
||||||
|
|
||||||
def invoice_access(request, access_code):
|
def invoice_access(request, access_code):
|
||||||
''' Redirects to the first unpaid invoice for the attendee that matches
|
''' Redirects to the first unpaid invoice for the attendee that matches
|
||||||
the given access code, if any. '''
|
the given access code, if any.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
access_code (castable to int): The access code for the user whose
|
||||||
|
invoice you want to see.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
redirect:
|
||||||
|
Redirect to the first unpaid invoice for that user.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Http404: If there is no such invoice.
|
||||||
|
'''
|
||||||
|
|
||||||
invoices = commerce.Invoice.objects.filter(
|
invoices = commerce.Invoice.objects.filter(
|
||||||
user__attendee__access_code=access_code,
|
user__attendee__access_code=access_code,
|
||||||
|
@ -475,10 +572,33 @@ def invoice_access(request, access_code):
|
||||||
|
|
||||||
|
|
||||||
def invoice(request, invoice_id, access_code=None):
|
def invoice(request, invoice_id, access_code=None):
|
||||||
''' Displays an invoice for a given invoice id.
|
''' Displays an invoice.
|
||||||
|
|
||||||
This view is not authenticated, but it will only allow access to either:
|
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
|
the user the invoice belongs to; staff; or a request made with the correct
|
||||||
access code.
|
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)
|
current_invoice = InvoiceController.for_id_or_404(invoice_id)
|
||||||
|
@ -498,7 +618,28 @@ def invoice(request, invoice_id, access_code=None):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def manual_payment(request, invoice_id):
|
def manual_payment(request, invoice_id):
|
||||||
''' Allows staff to make manual payments or refunds on an invoice.'''
|
''' 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Http404: if the logged in user is not staff.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
FORM_PREFIX = "manual_payment"
|
FORM_PREFIX = "manual_payment"
|
||||||
|
|
||||||
|
@ -528,8 +669,22 @@ def manual_payment(request, invoice_id):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def refund(request, invoice_id):
|
def refund(request, invoice_id):
|
||||||
''' Allows staff to refund payments against an invoice and request a
|
''' Marks an invoice as refunded and requests a credit note for the
|
||||||
credit note.'''
|
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``.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Http404: if the logged in user is not staff.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
if not request.user.is_staff:
|
if not request.user.is_staff:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
@ -545,9 +700,36 @@ def refund(request, invoice_id):
|
||||||
return redirect("invoice", invoice_id)
|
return redirect("invoice", invoice_id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
def credit_note(request, note_id, access_code=None):
|
def credit_note(request, note_id, access_code=None):
|
||||||
''' Displays an credit note for a given id.
|
''' Displays a credit note.
|
||||||
This view can only be seen by staff.
|
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Http404: If the logged in user is not staff.
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if not request.user.is_staff:
|
if not request.user.is_staff:
|
||||||
|
@ -584,7 +766,9 @@ def credit_note(request, note_id, access_code=None):
|
||||||
request,
|
request,
|
||||||
"Applied manual refund to credit note."
|
"Applied manual refund to credit note."
|
||||||
)
|
)
|
||||||
return redirect("invoice", invoice.id)
|
refund_form = forms.ManualCreditNoteRefundForm(
|
||||||
|
prefix="refund_note",
|
||||||
|
)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"credit_note": current_note.credit_note,
|
"credit_note": current_note.credit_note,
|
||||||
|
|
Loading…
Reference in a new issue