Changeset - 2ca644e5002d
[Not reviewed]
0 2 0
Christopher Neugebauer - 8 years ago 2016-09-15 02:25:34
chrisjrn@gmail.com
Adds form for generating a cancellation fee.
2 files changed with 27 insertions and 0 deletions:
0 comments (0 inline, 0 general)
registrasion/forms.py
Show inline comments
 
from registrasion.controllers.product import ProductController
 
from registrasion.models import commerce
 
from registrasion.models import inventory
 

	
 
from django import forms
 
from django.core.exceptions import ValidationError
 

	
 

	
 
class ApplyCreditNoteForm(forms.Form):
 

	
 
    def __init__(self, user, *a, **k):
 
        ''' User: The user whose invoices should be made available as
 
        choices. '''
 
        self.user = user
 
        super(ApplyCreditNoteForm, self).__init__(*a, **k)
 

	
 
        self.fields["invoice"].choices = self._unpaid_invoices_for_user
 

	
 
    def _unpaid_invoices_for_user(self):
 
        invoices = commerce.Invoice.objects.filter(
 
            status=commerce.Invoice.STATUS_UNPAID,
 
            user=self.user,
 
        )
 

	
 
        return [
 
            (invoice.id, "Invoice %(id)d - $%(value)d" % invoice.__dict__)
 
            for invoice in invoices
 
        ]
 

	
 
    invoice = forms.ChoiceField(
 
        required=True,
 
    )
 

	
 

	
 
class CancellationFeeForm(forms.Form):
 

	
 
    percentage = forms.DecimalField(
 
        required=True,
 
        min_value=0,
 
        max_value=100,
 
    )
 

	
 
class ManualCreditNoteRefundForm(forms.ModelForm):
 

	
 
    class Meta:
 
        model = commerce.ManualCreditNoteRefund
 
        fields = ["reference"]
 

	
 

	
 
class ManualPaymentForm(forms.ModelForm):
 

	
 
    class Meta:
 
        model = commerce.ManualPayment
 
        fields = ["reference", "amount"]
 

	
 

	
 
# Products forms -- none of these have any fields: they are to be subclassed
 
# and the fields added as needs be. ProductsForm (the function) is responsible
 
# for the subclassing.
 

	
 
def ProductsForm(category, products):
 
    ''' Produces an appropriate _ProductsForm subclass for the given render
 
    type. '''
 

	
 
    # Each Category.RENDER_TYPE value has a subclass here.
 
    cat = inventory.Category
 
    RENDER_TYPES = {
 
        cat.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm,
 
        cat.RENDER_TYPE_RADIO: _RadioButtonProductsForm,
 
        cat.RENDER_TYPE_ITEM_QUANTITY: _ItemQuantityProductsForm,
 
    }
 

	
 
    # Produce a subclass of _ProductsForm which we can alter the base_fields on
 
    class ProductsForm(RENDER_TYPES[category.render_type]):
 
        pass
 

	
 
    ProductsForm.set_fields(category, products)
 

	
 
    if category.render_type == inventory.Category.RENDER_TYPE_ITEM_QUANTITY:
 
        ProductsForm = forms.formset_factory(
 
            ProductsForm,
 
            formset=_ItemQuantityProductsFormSet,
 
        )
 

	
 
    return ProductsForm
 

	
 

	
 
class _HasProductsFields(object):
 

	
 
    PRODUCT_PREFIX = "product_"
 

	
 
    ''' Base class for product entry forms. '''
 
    def __init__(self, *a, **k):
 
        if "product_quantities" in k:
 
            initial = self.initial_data(k["product_quantities"])
 
            k["initial"] = initial
 
            del k["product_quantities"]
 
        super(_HasProductsFields, self).__init__(*a, **k)
 

	
 
    @classmethod
 
    def field_name(cls, product):
 
        return cls.PRODUCT_PREFIX + ("%d" % product.id)
 

	
 
    @classmethod
 
    def set_fields(cls, category, products):
 
        ''' Sets the base_fields on this _ProductsForm to allow selecting
 
        from the provided products. '''
 
        pass
 

	
 
    @classmethod
 
    def initial_data(cls, product_quantites):
 
        ''' Prepares initial data for an instance of this form.
 
        product_quantities is a sequence of (product,quantity) tuples '''
 
        return {}
 

	
 
    def product_quantities(self):
 
        ''' Yields a sequence of (product, quantity) tuples from the
 
        cleaned form data. '''
 
        return iter([])
 

	
 
    def add_product_error(self, product, error):
 
        ''' Adds an error to the given product's field '''
 

	
 
        ''' if product in field_names:
 
            field = field_names[product]
 
        elif isinstance(product, inventory.Product):
 
            return
 
        else:
 
            field = None '''
 

	
 
        self.add_error(self.field_name(product), error)
 

	
 

	
 
class _ProductsForm(_HasProductsFields, forms.Form):
 
    pass
 

	
 

	
 
class _QuantityBoxProductsForm(_ProductsForm):
 
    ''' Products entry form that allows users to enter quantities
 
    of desired products. '''
 

	
 
    @classmethod
 
    def set_fields(cls, category, products):
 
        for product in products:
 
            if product.description:
 
                help_text = "$%d each -- %s" % (
 
                    product.price,
 
                    product.description,
 
                )
 
            else:
 
                help_text = "$%d each" % product.price
 

	
 
            field = forms.IntegerField(
 
                label=product.name,
 
                help_text=help_text,
 
                min_value=0,
 
                max_value=500,  # Issue #19. We should figure out real limit.
 
            )
 
            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)] = 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, value)
 

	
 

	
 
class _RadioButtonProductsForm(_ProductsForm):
 
    ''' Products entry form that allows users to enter quantities
 
    of desired products. '''
 

	
 
    FIELD = "chosen_product"
 

	
 
    @classmethod
 
    def set_fields(cls, category, products):
 
        choices = []
 
        for product in products:
 
            choice_text = "%s -- $%d" % (product.name, product.price)
 
            choices.append((product.id, choice_text))
 

	
 
        if not category.required:
 
            choices.append((0, "No selection"))
 

	
 
        cls.base_fields[cls.FIELD] = forms.TypedChoiceField(
 
            label=category.name,
 
            widget=forms.RadioSelect,
 
            choices=choices,
 
            empty_value=0,
 
            coerce=int,
 
        )
 

	
 
    @classmethod
 
    def initial_data(cls, product_quantities):
 
        initial = {}
 

	
 
        for product, quantity in product_quantities:
 
            if quantity > 0:
 
                initial[cls.FIELD] = product.id
 
                break
 

	
 
        return initial
 

	
 
    def product_quantities(self):
 
        ours = self.cleaned_data[self.FIELD]
 
        choices = self.fields[self.FIELD].choices
 
        for choice_value, choice_display in choices:
 
            if choice_value == 0:
 
                continue
 
            yield (
 
                choice_value,
 
                1 if ours == choice_value else 0,
 
            )
 

	
 
    def add_product_error(self, product, error):
 
        self.add_error(self.FIELD, error)
 

	
 

	
 
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
 
     product type to be purchased. This form is usually used in concert with
 
     the _ItemQuantityProductsFormSet to allow selection of multiple
 
     products.'''
 

	
 
    CHOICE_FIELD = "choice"
 
    QUANTITY_FIELD = "quantity"
registrasion/views.py
Show inline comments
...
 
@@ -576,309 +576,327 @@ def _checkout_errors(request, errors):
 

	
 

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

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

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

	
 
    Arguments:
 

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

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

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

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

	
 
    if not invoices:
 
        raise Http404()
 

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

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

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

	
 

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

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

	
 
    Arguments:
 

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

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

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

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

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

	
 
    '''
 

	
 
    current_invoice = InvoiceController.for_id_or_404(invoice_id)
 

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

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

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

	
 

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

	
 

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

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

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

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

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

	
 
    '''
 

	
 
    FORM_PREFIX = "manual_payment"
 

	
 
    current_invoice = InvoiceController.for_id_or_404(invoice_id)
 

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

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

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

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

	
 

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

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

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

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

	
 
    '''
 

	
 
    current_invoice = InvoiceController.for_id_or_404(invoice_id)
 

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

	
 
    return redirect("invoice", invoice_id)
 

	
 

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

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

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

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

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

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

	
 
    '''
 

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

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

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

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

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

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

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

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

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

	
 

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

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

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

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

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

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

	
 
    if request.POST and formset.is_valid():
 

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

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

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

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

	
 
    return render(request, "registrasion/amend_registration.html", data)
0 comments (0 inline, 0 general)