From d4f4312178cd8c54bad8544570dc9fc98bb6b790 Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Thu, 15 Sep 2016 12:15:40 +1000
Subject: [PATCH 1/2] Adds cancellation fee implementation and tests

---
 registrasion/controllers/credit_note.py | 26 +++++++++++++++++++++++++
 registrasion/tests/test_credit_note.py  | 25 ++++++++++++++++++++++++
 2 files changed, 51 insertions(+)

diff --git a/registrasion/controllers/credit_note.py b/registrasion/controllers/credit_note.py
index 182c10e9..e4784c8b 100644
--- a/registrasion/controllers/credit_note.py
+++ b/registrasion/controllers/credit_note.py
@@ -1,3 +1,5 @@
+import datetime
+
 from django.db import transaction
 
 from registrasion.models import commerce
@@ -53,3 +55,27 @@ class CreditNoteController(ForId, object):
         inv.update_status()
 
     # TODO: Add administration fee generator.
+    @transaction.atomic
+    def cancellation_fee(self, percentage):
+        ''' Generates an invoice with a cancellation fee, and applies
+        credit to the invoice.
+
+        percentage (Decimal): The percentage of the credit note to turn into
+        a cancellation fee. Must be 0 <= percentage <= 100.
+        '''
+
+        from invoice import InvoiceController  # Circular imports bleh.
+
+        assert(percentage >= 0 and percentage <= 100)
+
+        cancellation_fee = self.credit_note.value * percentage / 100
+        due = datetime.timedelta(days=1)
+        item = [("Cancellation fee", cancellation_fee)]
+        invoice = InvoiceController.manual_invoice(
+            self.credit_note.invoice.user, due, item
+        )
+
+        if not invoice.is_paid:
+            self.apply_to_invoice(invoice)
+
+        return InvoiceController(invoice)
diff --git a/registrasion/tests/test_credit_note.py b/registrasion/tests/test_credit_note.py
index 2857ce7f..c4be1041 100644
--- a/registrasion/tests/test_credit_note.py
+++ b/registrasion/tests/test_credit_note.py
@@ -440,3 +440,28 @@ class CreditNoteTestCase(TestHelperMixin, RegistrationCartTestCase):
             # Generate invoice that should be automatically paid
             invoice2 = self._manual_invoice(1)
             self.assertTrue(invoice2.invoice.is_paid)
+
+    def test_cancellation_fee_is_applied(self):
+
+        invoice1 = self._manual_invoice(1)
+        invoice1.pay("Pay", invoice1.invoice.value)
+        invoice1.refund()
+
+        percentage = 15
+
+        cn = self._credit_note_for_invoice(invoice1.invoice)
+        canc = cn.cancellation_fee(15)
+
+        # Cancellation fee exceeds the amount for the invoice.
+        self.assertTrue(canc.invoice.is_paid)
+
+        # Cancellation fee is equal to 15% of credit note's value
+        self.assertEqual(
+            canc.invoice.value,
+            cn.credit_note.value * percentage / 100
+        )
+
+    def test_cancellation_fee_is_applied_when_another_invoice_is_unpaid(self):
+
+        extra_invoice = self._manual_invoice(23)
+        self.test_cancellation_fee_is_applied()

From 2ca644e5002d93218f0d827ba4dfe4ffe5811828 Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Thu, 15 Sep 2016 12:25:34 +1000
Subject: [PATCH 2/2] Adds form for generating a cancellation fee.

---
 registrasion/forms.py |  9 +++++++++
 registrasion/views.py | 18 ++++++++++++++++++
 2 files changed, 27 insertions(+)

diff --git a/registrasion/forms.py b/registrasion/forms.py
index cc555b78..4839b733 100644
--- a/registrasion/forms.py
+++ b/registrasion/forms.py
@@ -3,6 +3,7 @@ from registrasion.models import commerce
 from registrasion.models import inventory
 
 from django import forms
+from django.core.exceptions import ValidationError
 
 
 class ApplyCreditNoteForm(forms.Form):
@@ -31,6 +32,14 @@ class ApplyCreditNoteForm(forms.Form):
     )
 
 
+class CancellationFeeForm(forms.Form):
+
+    percentage = forms.DecimalField(
+        required=True,
+        min_value=0,
+        max_value=100,
+    )
+
 class ManualCreditNoteRefundForm(forms.ModelForm):
 
     class Meta:
diff --git a/registrasion/views.py b/registrasion/views.py
index f96104f1..9248dce4 100644
--- a/registrasion/views.py
+++ b/registrasion/views.py
@@ -765,6 +765,9 @@ def credit_note(request, note_id, access_code=None):
                                          # 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
                 }
 
     '''
@@ -783,6 +786,11 @@ def credit_note(request, note_id, access_code=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)
@@ -805,10 +813,20 @@ def credit_note(request, note_id, access_code=None):
             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)