Adds CreditNote, CreditNoteController, related models, and tests.
This commit is contained in:
		
							parent
							
								
									ae8f39381f
								
							
						
					
					
						commit
						6b10a0a7e4
					
				
					 10 changed files with 534 additions and 22 deletions
				
			
		
							
								
								
									
										51
									
								
								registrasion/controllers/credit_note.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								registrasion/controllers/credit_note.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | |||
| from django.db import transaction | ||||
| 
 | ||||
| from registrasion import models as rego | ||||
| 
 | ||||
| 
 | ||||
| class CreditNoteController(object): | ||||
| 
 | ||||
|     def __init__(self, credit_note): | ||||
|         self.credit_note = credit_note | ||||
| 
 | ||||
|     @classmethod | ||||
|     def generate_from_invoice(cls, invoice, value): | ||||
|         ''' Generates a credit note of the specified value and pays it against | ||||
|         the given invoice. You need to call InvoiceController.update_status() | ||||
|         to set the status correctly, if appropriate. ''' | ||||
| 
 | ||||
|         credit_note = rego.CreditNote.objects.create( | ||||
|             invoice=invoice, | ||||
|             amount=0-value,  # Credit notes start off as a payment against inv. | ||||
|             reference="ONE MOMENT", | ||||
|         ) | ||||
|         credit_note.reference = "Generated credit note %d" % credit_note.id | ||||
|         credit_note.save() | ||||
| 
 | ||||
|         return cls(credit_note) | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def apply_to_invoice(self, invoice): | ||||
|         ''' Applies the total value of this credit note to the specified | ||||
|         invoice. If this credit note overpays the invoice, a new credit note | ||||
|         containing the residual value will be created. | ||||
| 
 | ||||
|         Raises ValidationError if the given invoice is not allowed to be | ||||
|         paid. | ||||
|         ''' | ||||
| 
 | ||||
|         from invoice import InvoiceController  # Circular imports bleh. | ||||
|         inv = InvoiceController(invoice) | ||||
|         inv.validate_allowed_to_pay() | ||||
| 
 | ||||
|         # Apply payment to invoice | ||||
|         rego.CreditNoteApplication.objects.create( | ||||
|             parent=self.credit_note, | ||||
|             invoice=invoice, | ||||
|             amount=self.credit_note.value, | ||||
|             reference="Applied credit note #%d" % self.credit_note.id, | ||||
|         ) | ||||
| 
 | ||||
|         inv.update_status() | ||||
| 
 | ||||
|     # TODO: Add administration fee generator. | ||||
|  | @ -8,6 +8,7 @@ from django.utils import timezone | |||
| from registrasion import models as rego | ||||
| 
 | ||||
| from cart import CartController | ||||
| from credit_note import CreditNoteController | ||||
| 
 | ||||
| 
 | ||||
| class InvoiceController(object): | ||||
|  | @ -189,6 +190,12 @@ class InvoiceController(object): | |||
|             if remainder <= 0: | ||||
|                 # Invoice no longer has amount owing | ||||
|                 self._mark_paid() | ||||
| 
 | ||||
|                 if remainder < 0: | ||||
|                     CreditNoteController.generate_from_invoice( | ||||
|                         self.invoice, | ||||
|                         0 - remainder, | ||||
|                     ) | ||||
|             elif total_paid == 0 and num_payments > 0: | ||||
|                 # Invoice has multiple payments totalling zero | ||||
|                 self._mark_void() | ||||
|  | @ -247,30 +254,31 @@ class InvoiceController(object): | |||
| 
 | ||||
|     def void(self): | ||||
|         ''' Voids the invoice if it is valid to do so. ''' | ||||
|         if self.invoice.status == rego.Invoice.STATUS_PAID: | ||||
|             raise ValidationError("Paid invoices cannot be voided, " | ||||
|                                   "only refunded.") | ||||
|         if self.total_payments() > 0: | ||||
|             raise ValidationError("Invoices with payments must be refunded.") | ||||
|         elif self.invoice.is_refunded: | ||||
|             raise ValidationError("Refunded invoices may not be voided.") | ||||
|         self._mark_void() | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def refund(self, reference, amount): | ||||
|         ''' Refunds the invoice by the given amount. | ||||
|     def refund(self): | ||||
|         ''' Refunds the invoice by generating a CreditNote for the value of | ||||
|         all of the payments against the cart. | ||||
| 
 | ||||
|         The invoice is marked as refunded, and the underlying cart is marked | ||||
|         as released. | ||||
| 
 | ||||
|         TODO: replace with credit notes work instead. | ||||
|         ''' | ||||
| 
 | ||||
|         if self.invoice.is_void: | ||||
|             raise ValidationError("Void invoices cannot be refunded") | ||||
| 
 | ||||
|         # Adds a payment | ||||
|         # TODO: replace by creating a credit note instead | ||||
|         rego.ManualPayment.objects.create( | ||||
|             invoice=self.invoice, | ||||
|             reference=reference, | ||||
|             amount=0 - amount, | ||||
|         ) | ||||
|         # Raises a credit note fot the value of the invoice. | ||||
|         amount = self.total_payments() | ||||
| 
 | ||||
|         if amount == 0: | ||||
|             self.void() | ||||
|             return | ||||
| 
 | ||||
|         CreditNoteController.generate_from_invoice(self.invoice, amount) | ||||
|         self.update_status() | ||||
|  |  | |||
|  | @ -0,0 +1,43 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9.2 on 2016-04-10 07:54 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| import django.utils.timezone | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     replaces = [('registrasion', '0018_creditnote_creditnoteapplication_creditnoterefund'), ('registrasion', '0019_auto_20160410_0753')] | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('registrasion', '0017_auto_20160408_0731'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='CreditNote', | ||||
|             fields=[ | ||||
|                 ('paymentbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registrasion.PaymentBase')), | ||||
|             ], | ||||
|             bases=('registrasion.paymentbase',), | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='CreditNoteApplication', | ||||
|             fields=[ | ||||
|                 ('paymentbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registrasion.PaymentBase')), | ||||
|                 ('parent', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='registrasion.CreditNote')), | ||||
|             ], | ||||
|             bases=('registrasion.paymentbase',), | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='CreditNoteRefund', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('time', models.DateTimeField(default=django.utils.timezone.now)), | ||||
|                 ('reference', models.CharField(max_length=255)), | ||||
|                 ('parent', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='registrasion.CreditNote')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
|  | @ -0,0 +1,27 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9.2 on 2016-04-11 02:57 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     replaces = [('registrasion', '0019_manualcreditnoterefund'), ('registrasion', '0020_auto_20160411_0256')] | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|         ('registrasion', '0018_creditnote_creditnoteapplication_creditnoterefund_squashed_0019_auto_20160410_0753'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='ManualCreditNoteRefund', | ||||
|             fields=[ | ||||
|                 ('entered_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||
|                 ('creditnoterefund_ptr', models.OneToOneField(auto_created=True, default=0, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registrasion.CreditNoteRefund')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										21
									
								
								registrasion/migrations/0020_auto_20160411_0258.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								registrasion/migrations/0020_auto_20160411_0258.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9.2 on 2016-04-11 02:58 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('registrasion', '0019_manualcreditnoterefund_squashed_0020_auto_20160411_0256'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='manualcreditnoterefund', | ||||
|             name='creditnoterefund_ptr', | ||||
|             field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registrasion.CreditNoteRefund'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -648,3 +648,95 @@ class PaymentBase(models.Model): | |||
| class ManualPayment(PaymentBase): | ||||
|     ''' Payments that are manually entered by staff. ''' | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class CreditNote(PaymentBase): | ||||
|     ''' Credit notes represent money accounted for in the system that do not | ||||
|     belong to specific invoices. They may be paid into other invoices, or | ||||
|     cashed out as refunds. | ||||
| 
 | ||||
|     Each CreditNote may either be used to pay towards another Invoice in the | ||||
|     system (by attaching a CreditNoteApplication), or may be marked as | ||||
|     refunded (by attaching a CreditNoteRefund).''' | ||||
| 
 | ||||
|     @classmethod | ||||
|     def unclaimed(cls): | ||||
|         return cls.objects.filter( | ||||
|             creditnoteapplication=None, | ||||
|             creditnoterefund=None, | ||||
|         ) | ||||
| 
 | ||||
|     @property | ||||
|     def status(self): | ||||
|         if self.is_unclaimed: | ||||
|             return "Unclaimed" | ||||
| 
 | ||||
|         if hasattr(self, 'creditnoteapplication'): | ||||
|             destination = self.creditnoteapplication.invoice.id | ||||
|             return "Applied to invoice %d" % destination | ||||
| 
 | ||||
|         elif hasattr(self, 'creditnoterefund'): | ||||
|             reference = self.creditnoterefund.reference | ||||
|             print reference | ||||
|             return "Refunded with reference: %s" % reference | ||||
| 
 | ||||
|         raise ValueError("This should never happen.") | ||||
| 
 | ||||
|     @property | ||||
|     def is_unclaimed(self): | ||||
|         return not ( | ||||
|             hasattr(self, 'creditnoterefund') or | ||||
|             hasattr(self, 'creditnoteapplication') | ||||
|         ) | ||||
| 
 | ||||
|     @property | ||||
|     def value(self): | ||||
|         ''' Returns the value of the credit note. Because CreditNotes are | ||||
|         implemented as PaymentBase objects internally, the amount is a | ||||
|         negative payment against an invoice. ''' | ||||
|         return -self.amount | ||||
| 
 | ||||
| 
 | ||||
| class CleanOnSave(object): | ||||
| 
 | ||||
|     def save(self, *a, **k): | ||||
|         self.full_clean() | ||||
|         super(CleanOnSave, self).save(*a, **k) | ||||
| 
 | ||||
| 
 | ||||
| class CreditNoteApplication(CleanOnSave, PaymentBase): | ||||
|     ''' Represents an application of a credit note to an Invoice. ''' | ||||
| 
 | ||||
|     def clean(self): | ||||
|         if not hasattr(self, "parent"): | ||||
|             return | ||||
|         if hasattr(self.parent, 'creditnoterefund'): | ||||
|             raise ValidationError( | ||||
|                 "Cannot apply a refunded credit note to an invoice" | ||||
|             ) | ||||
| 
 | ||||
|     parent = models.OneToOneField(CreditNote) | ||||
| 
 | ||||
| 
 | ||||
| class CreditNoteRefund(CleanOnSave, models.Model): | ||||
|     ''' Represents a refund of a credit note to an external payment. | ||||
|     Credit notes may only be refunded in full. How those refunds are handled | ||||
|     is left as an exercise to the payment app. ''' | ||||
| 
 | ||||
|     def clean(self): | ||||
|         if not hasattr(self, "parent"): | ||||
|             return | ||||
|         if hasattr(self.parent, 'creditnoteapplication'): | ||||
|             raise ValidationError( | ||||
|                 "Cannot refund a credit note that has been paid to an invoice" | ||||
|             ) | ||||
| 
 | ||||
|     parent = models.OneToOneField(CreditNote) | ||||
|     time = models.DateTimeField(default=timezone.now) | ||||
|     reference = models.CharField(max_length=255) | ||||
| 
 | ||||
| 
 | ||||
| class ManualCreditNoteRefund(CreditNoteRefund): | ||||
|     ''' Credit notes that are entered by a staff member. ''' | ||||
| 
 | ||||
|     entered_by = models.ForeignKey(User) | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| from registrasion.controllers.cart import CartController | ||||
| from registrasion.controllers.credit_note import CreditNoteController | ||||
| from registrasion.controllers.invoice import InvoiceController | ||||
| from registrasion import models as rego | ||||
| 
 | ||||
|  | @ -47,3 +48,12 @@ class TestingInvoiceController(InvoiceController): | |||
|         ) | ||||
| 
 | ||||
|         self.update_status() | ||||
| 
 | ||||
| 
 | ||||
| class TestingCreditNoteController(CreditNoteController): | ||||
| 
 | ||||
|     def refund(self): | ||||
|         rego.CreditNoteRefund.objects.create( | ||||
|             parent=self.credit_note, | ||||
|             reference="Whoops." | ||||
|         ) | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ from django.core.exceptions import ValidationError | |||
| 
 | ||||
| from registrasion import models as rego | ||||
| from controller_helpers import TestingCartController | ||||
| from controller_helpers import TestingCreditNoteController | ||||
| from controller_helpers import TestingInvoiceController | ||||
| 
 | ||||
| from test_cart import RegistrationCartTestCase | ||||
|  | @ -187,12 +188,25 @@ class InvoiceTestCase(RegistrationCartTestCase): | |||
| 
 | ||||
|         # Should be able to create an invoice after the product is added | ||||
|         current_cart.add_to_cart(self.PROD_1, 1) | ||||
|         invoice_1 = TestingInvoiceController.for_cart(current_cart.cart) | ||||
|         invoice = TestingInvoiceController.for_cart(current_cart.cart) | ||||
| 
 | ||||
|         invoice_1.pay("Reference", invoice_1.invoice.value) | ||||
|         invoice.pay("Reference", invoice.invoice.value) | ||||
| 
 | ||||
|         with self.assertRaises(ValidationError): | ||||
|             invoice_1.void() | ||||
|             invoice.void() | ||||
| 
 | ||||
|     def test_cannot_void_partially_paid_invoice(self): | ||||
|         current_cart = TestingCartController.for_user(self.USER_1) | ||||
| 
 | ||||
|         # Should be able to create an invoice after the product is added | ||||
|         current_cart.add_to_cart(self.PROD_1, 1) | ||||
|         invoice = TestingInvoiceController.for_cart(current_cart.cart) | ||||
| 
 | ||||
|         invoice.pay("Reference", invoice.invoice.value - 1) | ||||
|         self.assertTrue(invoice.invoice.is_unpaid) | ||||
| 
 | ||||
|         with self.assertRaises(ValidationError): | ||||
|             invoice.void() | ||||
| 
 | ||||
|     def test_cannot_generate_blank_invoice(self): | ||||
|         current_cart = TestingCartController.for_user(self.USER_1) | ||||
|  | @ -210,9 +224,255 @@ class InvoiceTestCase(RegistrationCartTestCase): | |||
|         with self.assertRaises(ValidationError): | ||||
|             invoice.validate_allowed_to_pay() | ||||
| 
 | ||||
|     # TODO: test partially paid invoice cannot be void until payments | ||||
|     # are refunded | ||||
|     def test_overpaid_invoice_results_in_credit_note(self): | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|     # TODO: test overpaid invoice results in credit note | ||||
|         invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|     # TODO: test credit note generation more generally | ||||
|         # Invoice is overpaid by 1 unit | ||||
|         to_pay = invoice.invoice.value + 1 | ||||
|         invoice.pay("Reference", to_pay) | ||||
| 
 | ||||
|         # The total paid should be equal to the value of the invoice only | ||||
|         self.assertEqual(invoice.invoice.value, invoice.total_payments()) | ||||
|         self.assertTrue(invoice.invoice.is_paid) | ||||
| 
 | ||||
|         # There should be a credit note generated out of the invoice. | ||||
|         credit_notes = rego.CreditNote.objects.filter(invoice=invoice.invoice) | ||||
|         self.assertEqual(1, credit_notes.count()) | ||||
|         self.assertEqual(to_pay - invoice.invoice.value, credit_notes[0].value) | ||||
| 
 | ||||
|     def test_full_paid_invoice_does_not_generate_credit_note(self): | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         # Invoice is paid evenly | ||||
|         invoice.pay("Reference", invoice.invoice.value) | ||||
| 
 | ||||
|         # The total paid should be equal to the value of the invoice only | ||||
|         self.assertEqual(invoice.invoice.value, invoice.total_payments()) | ||||
|         self.assertTrue(invoice.invoice.is_paid) | ||||
| 
 | ||||
|         # There should be no credit notes | ||||
|         credit_notes = rego.CreditNote.objects.filter(invoice=invoice.invoice) | ||||
|         self.assertEqual(0, credit_notes.count()) | ||||
| 
 | ||||
|     def test_refund_partially_paid_invoice_generates_correct_credit_note(self): | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         # Invoice is underpaid by 1 unit | ||||
|         to_pay = invoice.invoice.value - 1 | ||||
|         invoice.pay("Reference", to_pay) | ||||
|         invoice.refund() | ||||
| 
 | ||||
|         # The total paid should be zero | ||||
|         self.assertEqual(0, invoice.total_payments()) | ||||
|         self.assertTrue(invoice.invoice.is_void) | ||||
| 
 | ||||
|         # There should be a credit note generated out of the invoice. | ||||
|         credit_notes = rego.CreditNote.objects.filter(invoice=invoice.invoice) | ||||
|         self.assertEqual(1, credit_notes.count()) | ||||
|         self.assertEqual(to_pay, credit_notes[0].value) | ||||
| 
 | ||||
|     def test_refund_fully_paid_invoice_generates_correct_credit_note(self): | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         to_pay = invoice.invoice.value | ||||
|         invoice.pay("Reference", to_pay) | ||||
|         self.assertTrue(invoice.invoice.is_paid) | ||||
| 
 | ||||
|         invoice.refund() | ||||
| 
 | ||||
|         # The total paid should be zero | ||||
|         self.assertEqual(0, invoice.total_payments()) | ||||
|         self.assertTrue(invoice.invoice.is_refunded) | ||||
| 
 | ||||
|         # There should be a credit note generated out of the invoice. | ||||
|         credit_notes = rego.CreditNote.objects.filter(invoice=invoice.invoice) | ||||
|         self.assertEqual(1, credit_notes.count()) | ||||
|         self.assertEqual(to_pay, credit_notes[0].value) | ||||
| 
 | ||||
|     def test_apply_credit_note_pays_invoice(self): | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         to_pay = invoice.invoice.value | ||||
|         invoice.pay("Reference", to_pay) | ||||
|         self.assertTrue(invoice.invoice.is_paid) | ||||
| 
 | ||||
|         invoice.refund() | ||||
| 
 | ||||
|         # There should be one credit note generated out of the invoice. | ||||
|         credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice) | ||||
|         cn = TestingCreditNoteController(credit_note) | ||||
| 
 | ||||
|         # That credit note should be in the unclaimed pile | ||||
|         self.assertEquals(1, rego.CreditNote.unclaimed().count()) | ||||
| 
 | ||||
|         # Create a new (identical) cart with invoice | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice2 = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         cn.apply_to_invoice(invoice2.invoice) | ||||
|         self.assertTrue(invoice2.invoice.is_paid) | ||||
| 
 | ||||
|         # That invoice should not show up as unclaimed any more | ||||
|         self.assertEquals(0, rego.CreditNote.unclaimed().count()) | ||||
| 
 | ||||
|     def test_apply_credit_note_generates_new_credit_note_if_overpaying(self): | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 2) | ||||
| 
 | ||||
|         invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         to_pay = invoice.invoice.value | ||||
|         invoice.pay("Reference", to_pay) | ||||
|         self.assertTrue(invoice.invoice.is_paid) | ||||
| 
 | ||||
|         invoice.refund() | ||||
| 
 | ||||
|         # There should be one credit note generated out of the invoice. | ||||
|         credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice) | ||||
|         cn = TestingCreditNoteController(credit_note) | ||||
| 
 | ||||
|         self.assertEquals(1, rego.CreditNote.unclaimed().count()) | ||||
| 
 | ||||
|         # Create a new cart (of half value of inv 1) and get invoice | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice2 = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         cn.apply_to_invoice(invoice2.invoice) | ||||
|         self.assertTrue(invoice2.invoice.is_paid) | ||||
| 
 | ||||
|         # We generated a new credit note, and spent the old one, | ||||
|         # unclaimed should still be 1. | ||||
|         self.assertEquals(1, rego.CreditNote.unclaimed().count()) | ||||
| 
 | ||||
|         credit_note2 = rego.CreditNote.objects.get(invoice=invoice2.invoice) | ||||
| 
 | ||||
|         # The new credit note should be the residual of the cost of cart 1 | ||||
|         # minus the cost of cart 2. | ||||
|         self.assertEquals( | ||||
|             invoice.invoice.value - invoice2.invoice.value, | ||||
|             credit_note2.value, | ||||
|         ) | ||||
| 
 | ||||
|     def test_cannot_apply_credit_note_on_invalid_invoices(self): | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         to_pay = invoice.invoice.value | ||||
|         invoice.pay("Reference", to_pay) | ||||
|         self.assertTrue(invoice.invoice.is_paid) | ||||
| 
 | ||||
|         invoice.refund() | ||||
| 
 | ||||
|         # There should be one credit note generated out of the invoice. | ||||
|         credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice) | ||||
|         cn = TestingCreditNoteController(credit_note) | ||||
| 
 | ||||
|         # Create a new cart with invoice, pay it | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
|         invoice_2.pay("LOL", invoice_2.invoice.value) | ||||
| 
 | ||||
|         # Cannot pay paid invoice | ||||
|         with self.assertRaises(ValidationError): | ||||
|             cn.apply_to_invoice(invoice_2.invoice) | ||||
| 
 | ||||
|         invoice_2.refund() | ||||
|         # Cannot pay refunded invoice | ||||
|         with self.assertRaises(ValidationError): | ||||
|             cn.apply_to_invoice(invoice_2.invoice) | ||||
| 
 | ||||
|         # Create a new cart with invoice | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
|         invoice_2.void() | ||||
|         # Cannot pay void invoice | ||||
|         with self.assertRaises(ValidationError): | ||||
|             cn.apply_to_invoice(invoice_2.invoice) | ||||
| 
 | ||||
|     def test_cannot_apply_a_refunded_credit_note(self): | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         to_pay = invoice.invoice.value | ||||
|         invoice.pay("Reference", to_pay) | ||||
|         self.assertTrue(invoice.invoice.is_paid) | ||||
| 
 | ||||
|         invoice.refund() | ||||
| 
 | ||||
|         self.assertEquals(1, rego.CreditNote.unclaimed().count()) | ||||
| 
 | ||||
|         credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice) | ||||
| 
 | ||||
|         cn = TestingCreditNoteController(credit_note) | ||||
|         cn.refund() | ||||
| 
 | ||||
|         # Refunding a credit note should mark it as claimed | ||||
|         self.assertEquals(0, rego.CreditNote.unclaimed().count()) | ||||
| 
 | ||||
|         # Create a new cart with invoice | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         # Cannot pay with this credit note. | ||||
|         with self.assertRaises(ValidationError): | ||||
|             cn.apply_to_invoice(invoice_2.invoice) | ||||
| 
 | ||||
|     def test_cannot_refund_an_applied_credit_note(self): | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
| 
 | ||||
|         to_pay = invoice.invoice.value | ||||
|         invoice.pay("Reference", to_pay) | ||||
|         self.assertTrue(invoice.invoice.is_paid) | ||||
| 
 | ||||
|         invoice.refund() | ||||
| 
 | ||||
|         self.assertEquals(1, rego.CreditNote.unclaimed().count()) | ||||
| 
 | ||||
|         credit_note = rego.CreditNote.objects.get(invoice=invoice.invoice) | ||||
| 
 | ||||
|         cn = TestingCreditNoteController(credit_note) | ||||
| 
 | ||||
|         # Create a new cart with invoice | ||||
|         cart = TestingCartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart)) | ||||
|         cn.apply_to_invoice(invoice_2.invoice) | ||||
| 
 | ||||
|         self.assertEquals(0, rego.CreditNote.unclaimed().count()) | ||||
| 
 | ||||
|         # Cannot refund this credit note as it is already applied. | ||||
|         with self.assertRaises(ValidationError): | ||||
|             cn.refund() | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ class RefundTestCase(RegistrationCartTestCase): | |||
|         self.assertFalse(invoice.invoice.is_refunded) | ||||
|         self.assertFalse(invoice.invoice.cart.released) | ||||
| 
 | ||||
|         invoice.refund("A Refund!", invoice.invoice.value) | ||||
|         invoice.refund() | ||||
|         self.assertFalse(invoice.invoice.is_void) | ||||
|         self.assertFalse(invoice.invoice.is_paid) | ||||
|         self.assertTrue(invoice.invoice.is_refunded) | ||||
|  |  | |||
|  | @ -148,7 +148,7 @@ class VoucherTestCases(RegistrationCartTestCase): | |||
|         with self.assertRaises(ValidationError): | ||||
|             current_cart.apply_voucher(voucher.code) | ||||
| 
 | ||||
|         inv.refund("Hello!", inv.invoice.value) | ||||
|         inv.refund() | ||||
|         current_cart.apply_voucher(voucher.code) | ||||
| 
 | ||||
|     def test_fix_simple_errors_does_not_remove_limited_voucher(self): | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer