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 registrasion import models as rego
|
||||||
|
|
||||||
from cart import CartController
|
from cart import CartController
|
||||||
|
from credit_note import CreditNoteController
|
||||||
|
|
||||||
|
|
||||||
class InvoiceController(object):
|
class InvoiceController(object):
|
||||||
|
@ -189,6 +190,12 @@ class InvoiceController(object):
|
||||||
if remainder <= 0:
|
if remainder <= 0:
|
||||||
# Invoice no longer has amount owing
|
# Invoice no longer has amount owing
|
||||||
self._mark_paid()
|
self._mark_paid()
|
||||||
|
|
||||||
|
if remainder < 0:
|
||||||
|
CreditNoteController.generate_from_invoice(
|
||||||
|
self.invoice,
|
||||||
|
0 - remainder,
|
||||||
|
)
|
||||||
elif total_paid == 0 and num_payments > 0:
|
elif total_paid == 0 and num_payments > 0:
|
||||||
# Invoice has multiple payments totalling zero
|
# Invoice has multiple payments totalling zero
|
||||||
self._mark_void()
|
self._mark_void()
|
||||||
|
@ -247,30 +254,31 @@ class InvoiceController(object):
|
||||||
|
|
||||||
def void(self):
|
def void(self):
|
||||||
''' Voids the invoice if it is valid to do so. '''
|
''' Voids the invoice if it is valid to do so. '''
|
||||||
if self.invoice.status == rego.Invoice.STATUS_PAID:
|
if self.total_payments() > 0:
|
||||||
raise ValidationError("Paid invoices cannot be voided, "
|
raise ValidationError("Invoices with payments must be refunded.")
|
||||||
"only refunded.")
|
elif self.invoice.is_refunded:
|
||||||
|
raise ValidationError("Refunded invoices may not be voided.")
|
||||||
self._mark_void()
|
self._mark_void()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def refund(self, reference, amount):
|
def refund(self):
|
||||||
''' Refunds the invoice by the given amount.
|
''' 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
|
The invoice is marked as refunded, and the underlying cart is marked
|
||||||
as released.
|
as released.
|
||||||
|
|
||||||
TODO: replace with credit notes work instead.
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if self.invoice.is_void:
|
if self.invoice.is_void:
|
||||||
raise ValidationError("Void invoices cannot be refunded")
|
raise ValidationError("Void invoices cannot be refunded")
|
||||||
|
|
||||||
# Adds a payment
|
# Raises a credit note fot the value of the invoice.
|
||||||
# TODO: replace by creating a credit note instead
|
amount = self.total_payments()
|
||||||
rego.ManualPayment.objects.create(
|
|
||||||
invoice=self.invoice,
|
|
||||||
reference=reference,
|
|
||||||
amount=0 - amount,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if amount == 0:
|
||||||
|
self.void()
|
||||||
|
return
|
||||||
|
|
||||||
|
CreditNoteController.generate_from_invoice(self.invoice, amount)
|
||||||
self.update_status()
|
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):
|
class ManualPayment(PaymentBase):
|
||||||
''' Payments that are manually entered by staff. '''
|
''' Payments that are manually entered by staff. '''
|
||||||
pass
|
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.cart import CartController
|
||||||
|
from registrasion.controllers.credit_note import CreditNoteController
|
||||||
from registrasion.controllers.invoice import InvoiceController
|
from registrasion.controllers.invoice import InvoiceController
|
||||||
from registrasion import models as rego
|
from registrasion import models as rego
|
||||||
|
|
||||||
|
@ -47,3 +48,12 @@ class TestingInvoiceController(InvoiceController):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.update_status()
|
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 registrasion import models as rego
|
||||||
from controller_helpers import TestingCartController
|
from controller_helpers import TestingCartController
|
||||||
|
from controller_helpers import TestingCreditNoteController
|
||||||
from controller_helpers import TestingInvoiceController
|
from controller_helpers import TestingInvoiceController
|
||||||
|
|
||||||
from test_cart import RegistrationCartTestCase
|
from test_cart import RegistrationCartTestCase
|
||||||
|
@ -187,12 +188,25 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
|
|
||||||
# Should be able to create an invoice after the product is added
|
# Should be able to create an invoice after the product is added
|
||||||
current_cart.add_to_cart(self.PROD_1, 1)
|
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):
|
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):
|
def test_cannot_generate_blank_invoice(self):
|
||||||
current_cart = TestingCartController.for_user(self.USER_1)
|
current_cart = TestingCartController.for_user(self.USER_1)
|
||||||
|
@ -210,9 +224,255 @@ class InvoiceTestCase(RegistrationCartTestCase):
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
invoice.validate_allowed_to_pay()
|
invoice.validate_allowed_to_pay()
|
||||||
|
|
||||||
# TODO: test partially paid invoice cannot be void until payments
|
def test_overpaid_invoice_results_in_credit_note(self):
|
||||||
# are refunded
|
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.is_refunded)
|
||||||
self.assertFalse(invoice.invoice.cart.released)
|
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_void)
|
||||||
self.assertFalse(invoice.invoice.is_paid)
|
self.assertFalse(invoice.invoice.is_paid)
|
||||||
self.assertTrue(invoice.invoice.is_refunded)
|
self.assertTrue(invoice.invoice.is_refunded)
|
||||||
|
|
|
@ -148,7 +148,7 @@ class VoucherTestCases(RegistrationCartTestCase):
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
current_cart.apply_voucher(voucher.code)
|
current_cart.apply_voucher(voucher.code)
|
||||||
|
|
||||||
inv.refund("Hello!", inv.invoice.value)
|
inv.refund()
|
||||||
current_cart.apply_voucher(voucher.code)
|
current_cart.apply_voucher(voucher.code)
|
||||||
|
|
||||||
def test_fix_simple_errors_does_not_remove_limited_voucher(self):
|
def test_fix_simple_errors_does_not_remove_limited_voucher(self):
|
||||||
|
|
Loading…
Reference in a new issue