Makes invoice model, controller, and test changes to match issue #15 design doc
This commit is contained in:
		
							parent
							
								
									5633554854
								
							
						
					
					
						commit
						38cdb8aa63
					
				
					 7 changed files with 313 additions and 99 deletions
				
			
		|  | @ -3,6 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist | ||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| from django.db.models import Sum | from django.db.models import Sum | ||||||
|  | from django.utils import timezone | ||||||
| 
 | 
 | ||||||
| from registrasion import models as rego | from registrasion import models as rego | ||||||
| 
 | 
 | ||||||
|  | @ -13,6 +14,7 @@ class InvoiceController(object): | ||||||
| 
 | 
 | ||||||
|     def __init__(self, invoice): |     def __init__(self, invoice): | ||||||
|         self.invoice = invoice |         self.invoice = invoice | ||||||
|  |         self.update_status() | ||||||
|         self.update_validity()  # Make sure this invoice is up-to-date |         self.update_validity()  # Make sure this invoice is up-to-date | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|  | @ -22,21 +24,26 @@ class InvoiceController(object): | ||||||
|         an invoice is generated.''' |         an invoice is generated.''' | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|             invoice = rego.Invoice.objects.get( |             invoice = rego.Invoice.objects.exclude( | ||||||
|  |                 status=rego.Invoice.STATUS_VOID, | ||||||
|  |             ).get( | ||||||
|                 cart=cart, |                 cart=cart, | ||||||
|                 cart_revision=cart.revision, |                 cart_revision=cart.revision, | ||||||
|                 void=False, |  | ||||||
|             ) |             ) | ||||||
|         except ObjectDoesNotExist: |         except ObjectDoesNotExist: | ||||||
|             cart_controller = CartController(cart) |             cart_controller = CartController(cart) | ||||||
|             cart_controller.validate_cart()  # Raises ValidationError on fail. |             cart_controller.validate_cart()  # Raises ValidationError on fail. | ||||||
| 
 | 
 | ||||||
|             # Void past invoices for this cart |             cls.void_all_invoices(cart) | ||||||
|             rego.Invoice.objects.filter(cart=cart).update(void=True) |  | ||||||
| 
 |  | ||||||
|             invoice = cls._generate(cart) |             invoice = cls._generate(cart) | ||||||
| 
 | 
 | ||||||
|         return InvoiceController(invoice) |         return cls(invoice) | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def void_all_invoices(cls, cart): | ||||||
|  |         invoices = rego.Invoice.objects.filter(cart=cart).all() | ||||||
|  |         for invoice in invoices: | ||||||
|  |             cls(invoice).void() | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def resolve_discount_value(cls, item): |     def resolve_discount_value(cls, item): | ||||||
|  | @ -60,11 +67,21 @@ class InvoiceController(object): | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def _generate(cls, cart): |     def _generate(cls, cart): | ||||||
|         ''' Generates an invoice for the given cart. ''' |         ''' Generates an invoice for the given cart. ''' | ||||||
|  | 
 | ||||||
|  |         issued = timezone.now() | ||||||
|  |         reservation_limit = cart.reservation_duration + cart.time_last_updated | ||||||
|  |         # Never generate a due time that is before the issue time | ||||||
|  |         due = max(issued, reservation_limit) | ||||||
|  | 
 | ||||||
|         invoice = rego.Invoice.objects.create( |         invoice = rego.Invoice.objects.create( | ||||||
|             user=cart.user, |             user=cart.user, | ||||||
|             cart=cart, |             cart=cart, | ||||||
|             cart_revision=cart.revision, |             cart_revision=cart.revision, | ||||||
|             value=Decimal() |             status=rego.Invoice.STATUS_UNPAID, | ||||||
|  |             value=Decimal(), | ||||||
|  |             issue_time=issued, | ||||||
|  |             due_time=due, | ||||||
|  |             recipient="BOB_THOMAS", # TODO: add recipient generating code | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         product_items = rego.ProductItem.objects.filter(cart=cart) |         product_items = rego.ProductItem.objects.filter(cart=cart) | ||||||
|  | @ -84,6 +101,7 @@ class InvoiceController(object): | ||||||
|                 description="%s - %s" % (product.category.name, product.name), |                 description="%s - %s" % (product.category.name, product.name), | ||||||
|                 quantity=item.quantity, |                 quantity=item.quantity, | ||||||
|                 price=product.price, |                 price=product.price, | ||||||
|  |                 product=product, | ||||||
|             ) |             ) | ||||||
|             invoice_value += line_item.quantity * line_item.price |             invoice_value += line_item.quantity * line_item.price | ||||||
| 
 | 
 | ||||||
|  | @ -93,94 +111,121 @@ class InvoiceController(object): | ||||||
|                 description=item.discount.description, |                 description=item.discount.description, | ||||||
|                 quantity=item.quantity, |                 quantity=item.quantity, | ||||||
|                 price=cls.resolve_discount_value(item) * -1, |                 price=cls.resolve_discount_value(item) * -1, | ||||||
|  |                 product=item.product, | ||||||
|             ) |             ) | ||||||
|             invoice_value += line_item.quantity * line_item.price |             invoice_value += line_item.quantity * line_item.price | ||||||
| 
 | 
 | ||||||
|         invoice.value = invoice_value |         invoice.value = invoice_value | ||||||
| 
 | 
 | ||||||
|         if invoice.value == 0: |  | ||||||
|             invoice.paid = True |  | ||||||
| 
 |  | ||||||
|         invoice.save() |         invoice.save() | ||||||
| 
 | 
 | ||||||
|         return invoice |         return invoice | ||||||
| 
 | 
 | ||||||
|  |     def total_payments(self): | ||||||
|  |         ''' Returns the total amount paid towards this invoice. ''' | ||||||
|  | 
 | ||||||
|  |         payments = rego.PaymentBase.objects.filter(invoice=self.invoice) | ||||||
|  |         total_paid = payments.aggregate(Sum("amount"))["amount__sum"] or 0 | ||||||
|  |         return total_paid | ||||||
|  | 
 | ||||||
|  |     def update_status(self): | ||||||
|  |         ''' Updates the status of this invoice based upon the total | ||||||
|  |         payments.''' | ||||||
|  | 
 | ||||||
|  |         old_status = self.invoice.status | ||||||
|  |         total_paid = self.total_payments() | ||||||
|  |         num_payments = rego.PaymentBase.objects.filter( | ||||||
|  |             invoice=self.invoice, | ||||||
|  |         ).count() | ||||||
|  |         remainder = self.invoice.value - total_paid | ||||||
|  | 
 | ||||||
|  |         if old_status == rego.Invoice.STATUS_UNPAID: | ||||||
|  |             # Invoice had an amount owing | ||||||
|  |             if remainder <= 0: | ||||||
|  |                 # Invoice no longer has amount owing | ||||||
|  |                 self._mark_paid() | ||||||
|  |             elif total_paid == 0 and num_payments > 0: | ||||||
|  |                 # Invoice has multiple payments totalling zero | ||||||
|  |                 self._mark_void() | ||||||
|  |         elif old_status == rego.Invoice.STATUS_PAID: | ||||||
|  |             if remainder > 0: | ||||||
|  |                 # Invoice went from having a remainder of zero or less | ||||||
|  |                 # to having a positive remainder -- must be a refund | ||||||
|  |                 self._mark_refunded() | ||||||
|  |         elif old_status == rego.Invoice.STATUS_REFUNDED: | ||||||
|  |             # Should not ever change from here | ||||||
|  |             pass | ||||||
|  |         elif old_status == rego.Invoice.STATUS_VOID: | ||||||
|  |             # Should not ever change from here | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |     def _mark_paid(self): | ||||||
|  |         ''' Marks the invoice as paid, and updates the attached cart if | ||||||
|  |         necessary. ''' | ||||||
|  |         cart = self.invoice.cart | ||||||
|  |         if cart: | ||||||
|  |             cart.active = False | ||||||
|  |             cart.save() | ||||||
|  |         self.invoice.status = rego.Invoice.STATUS_PAID | ||||||
|  |         self.invoice.save() | ||||||
|  | 
 | ||||||
|  |     def _mark_refunded(self): | ||||||
|  |         ''' Marks the invoice as refunded, and updates the attached cart if | ||||||
|  |         necessary. ''' | ||||||
|  |         cart = self.invoice.cart | ||||||
|  |         if cart: | ||||||
|  |             cart.active = False | ||||||
|  |             cart.released = True | ||||||
|  |             cart.save() | ||||||
|  |         self.invoice.status = rego.Invoice.STATUS_REFUNDED | ||||||
|  |         self.invoice.save() | ||||||
|  | 
 | ||||||
|  |     def _mark_void(self): | ||||||
|  |         ''' Marks the invoice as refunded, and updates the attached cart if | ||||||
|  |         necessary. ''' | ||||||
|  |         self.invoice.status = rego.Invoice.STATUS_VOID | ||||||
|  |         self.invoice.save() | ||||||
|  | 
 | ||||||
|  |     def _invoice_matches_cart(self): | ||||||
|  |         ''' Returns true if there is no cart, or if the revision of this | ||||||
|  |         invoice matches the current revision of the cart. ''' | ||||||
|  |         cart = self.invoice.cart | ||||||
|  |         if not cart: | ||||||
|  |             return True | ||||||
|  | 
 | ||||||
|  |         return cart.revision == self.invoice.cart_revision | ||||||
|  | 
 | ||||||
|     def update_validity(self): |     def update_validity(self): | ||||||
|         ''' Updates the validity of this invoice if the cart it is attached to |         ''' Voids this invoice if the cart it is attached to has updated. ''' | ||||||
|         has updated. ''' |         if not self._invoice_matches_cart(): | ||||||
|         if self.invoice.cart is not None: |             self.void() | ||||||
|             if self.invoice.cart.revision != self.invoice.cart_revision: |  | ||||||
|                 self.void() |  | ||||||
| 
 | 
 | ||||||
|     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.paid: |         if self.invoice.status == rego.Invoice.STATUS_PAID: | ||||||
|             raise ValidationError("Paid invoices cannot be voided, " |             raise ValidationError("Paid invoices cannot be voided, " | ||||||
|                                   "only refunded.") |                                   "only refunded.") | ||||||
|         self.invoice.void = True |         self._mark_void() | ||||||
|         self.invoice.save() |  | ||||||
| 
 |  | ||||||
|     @transaction.atomic |  | ||||||
|     def pay(self, reference, amount): |  | ||||||
|         ''' Pays the invoice by the given amount. If the payment |  | ||||||
|         equals the total on the invoice, finalise the invoice. |  | ||||||
|         (NB should be transactional.) |  | ||||||
|         ''' |  | ||||||
|         if self.invoice.cart: |  | ||||||
|             cart = CartController(self.invoice.cart) |  | ||||||
|             cart.validate_cart()  # Raises ValidationError if invalid |  | ||||||
| 
 |  | ||||||
|         if self.invoice.void: |  | ||||||
|             raise ValidationError("Void invoices cannot be paid") |  | ||||||
| 
 |  | ||||||
|         if self.invoice.paid: |  | ||||||
|             raise ValidationError("Paid invoices cannot be paid again") |  | ||||||
| 
 |  | ||||||
|         ''' Adds a payment ''' |  | ||||||
|         payment = rego.Payment.objects.create( |  | ||||||
|             invoice=self.invoice, |  | ||||||
|             reference=reference, |  | ||||||
|             amount=amount, |  | ||||||
|         ) |  | ||||||
|         payment.save() |  | ||||||
| 
 |  | ||||||
|         payments = rego.Payment.objects.filter(invoice=self.invoice) |  | ||||||
|         agg = payments.aggregate(Sum("amount")) |  | ||||||
|         total = agg["amount__sum"] |  | ||||||
| 
 |  | ||||||
|         if total == self.invoice.value: |  | ||||||
|             self.invoice.paid = True |  | ||||||
| 
 |  | ||||||
|             if self.invoice.cart: |  | ||||||
|                 cart = self.invoice.cart |  | ||||||
|                 cart.active = False |  | ||||||
|                 cart.save() |  | ||||||
| 
 |  | ||||||
|             self.invoice.save() |  | ||||||
| 
 | 
 | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def refund(self, reference, amount): |     def refund(self, reference, amount): | ||||||
|         ''' Refunds the invoice by the given amount. The invoice is |         ''' Refunds the invoice by the given amount. | ||||||
|         marked as unpaid, and the underlying cart is marked as released. | 
 | ||||||
|  |         The invoice is marked as refunded, and the underlying cart is marked | ||||||
|  |         as released. | ||||||
|  | 
 | ||||||
|  |         TODO: replace with credit notes work instead. | ||||||
|         ''' |         ''' | ||||||
| 
 | 
 | ||||||
|         if self.invoice.void: |         if self.invoice.is_void: | ||||||
|             raise ValidationError("Void invoices cannot be refunded") |             raise ValidationError("Void invoices cannot be refunded") | ||||||
| 
 | 
 | ||||||
|         ''' Adds a payment ''' |         # Adds a payment | ||||||
|         payment = rego.Payment.objects.create( |         # TODO: replace by creating a credit note instead | ||||||
|  |         rego.ManualPayment.objects.create( | ||||||
|             invoice=self.invoice, |             invoice=self.invoice, | ||||||
|             reference=reference, |             reference=reference, | ||||||
|             amount=0 - amount, |             amount=0 - amount, | ||||||
|         ) |         ) | ||||||
|         payment.save() |  | ||||||
| 
 | 
 | ||||||
|         self.invoice.paid = False |         self.update_status() | ||||||
|         self.invoice.void = True |  | ||||||
| 
 |  | ||||||
|         if self.invoice.cart: |  | ||||||
|             cart = self.invoice.cart |  | ||||||
|             cart.released = True |  | ||||||
|             cart.save() |  | ||||||
| 
 |  | ||||||
|         self.invoice.save() |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,88 @@ | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Generated by Django 1.9.2 on 2016-04-07 03:13 | ||||||
|  | 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', '0013_auto_20160406_2228'), ('registrasion', '0014_auto_20160406_1847'), ('registrasion', '0015_auto_20160406_1942')] | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         ('registrasion', '0012_auto_20160406_1212'), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='PaymentBase', | ||||||
|  |             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)), | ||||||
|  |                 ('amount', models.DecimalField(decimal_places=2, max_digits=8)), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |         migrations.RemoveField( | ||||||
|  |             model_name='payment', | ||||||
|  |             name='invoice', | ||||||
|  |         ), | ||||||
|  |         migrations.RemoveField( | ||||||
|  |             model_name='invoice', | ||||||
|  |             name='paid', | ||||||
|  |         ), | ||||||
|  |         migrations.RemoveField( | ||||||
|  |             model_name='invoice', | ||||||
|  |             name='void', | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='invoice', | ||||||
|  |             name='due_time', | ||||||
|  |             field=models.DateTimeField(default=django.utils.timezone.now), | ||||||
|  |             preserve_default=False, | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='invoice', | ||||||
|  |             name='issue_time', | ||||||
|  |             field=models.DateTimeField(default=django.utils.timezone.now), | ||||||
|  |             preserve_default=False, | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='invoice', | ||||||
|  |             name='recipient', | ||||||
|  |             field=models.CharField(default='Lol', max_length=1024), | ||||||
|  |             preserve_default=False, | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='invoice', | ||||||
|  |             name='status', | ||||||
|  |             field=models.IntegerField(choices=[(1, 'Unpaid'), (2, 'Paid'), (3, 'Refunded'), (4, 'VOID')], db_index=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='lineitem', | ||||||
|  |             name='product', | ||||||
|  |             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='registrasion.Product'), | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='ManualPayment', | ||||||
|  |             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.DeleteModel( | ||||||
|  |             name='Payment', | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='paymentbase', | ||||||
|  |             name='invoice', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='registrasion.Invoice'), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='invoice', | ||||||
|  |             name='cart_revision', | ||||||
|  |             field=models.IntegerField(db_index=True, null=True), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
|  | @ -3,6 +3,7 @@ from __future__ import unicode_literals | ||||||
| import datetime | import datetime | ||||||
| import itertools | import itertools | ||||||
| 
 | 
 | ||||||
|  | from django.core.exceptions import ObjectDoesNotExist | ||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.db import models | from django.db import models | ||||||
|  | @ -26,13 +27,10 @@ class Attendee(models.Model): | ||||||
|     def get_instance(user): |     def get_instance(user): | ||||||
|         ''' Returns the instance of attendee for the given user, or creates |         ''' Returns the instance of attendee for the given user, or creates | ||||||
|         a new one. ''' |         a new one. ''' | ||||||
|         attendees = Attendee.objects.filter(user=user) |         try: | ||||||
|         if len(attendees) > 0: |             return Attendee.objects.get(user=user) | ||||||
|             return attendees[0] |         except ObjectDoesNotExist: | ||||||
|         else: |             return Attendee.objects.create(user=user) | ||||||
|             attendee = Attendee(user=user) |  | ||||||
|             attendee.save() |  | ||||||
|             return attendee |  | ||||||
| 
 | 
 | ||||||
|     user = models.OneToOneField(User, on_delete=models.CASCADE) |     user = models.OneToOneField(User, on_delete=models.CASCADE) | ||||||
|     # Badge/profile is linked |     # Badge/profile is linked | ||||||
|  | @ -54,6 +52,19 @@ class AttendeeProfileBase(models.Model): | ||||||
|         speaker profile. If it's None, that functionality is disabled. ''' |         speaker profile. If it's None, that functionality is disabled. ''' | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
|  |     def invoice_recipient(self): | ||||||
|  |         ''' Returns a representation of this attendee profile for the purpose | ||||||
|  |         of rendering to an invoice. Override in subclasses. ''' | ||||||
|  | 
 | ||||||
|  |         # Manual dispatch to subclass. Fleh. | ||||||
|  |         slf = AttendeeProfileBase.objects.get_subclass(id=self.id) | ||||||
|  |         # Actually compare the functions. | ||||||
|  |         if type(slf).invoice_recipient != type(self).invoice_recipient: | ||||||
|  |             return type(slf).invoice_recipient(slf) | ||||||
|  | 
 | ||||||
|  |         # Return a default | ||||||
|  |         return slf.attendee.user.username | ||||||
|  | 
 | ||||||
|     attendee = models.OneToOneField(Attendee, on_delete=models.CASCADE) |     attendee = models.OneToOneField(Attendee, on_delete=models.CASCADE) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -533,6 +544,18 @@ class Invoice(models.Model): | ||||||
|     ''' An invoice. Invoices can be automatically generated when checking out |     ''' An invoice. Invoices can be automatically generated when checking out | ||||||
|     a Cart, in which case, it is attached to a given revision of a Cart. ''' |     a Cart, in which case, it is attached to a given revision of a Cart. ''' | ||||||
| 
 | 
 | ||||||
|  |     STATUS_UNPAID = 1 | ||||||
|  |     STATUS_PAID = 2 | ||||||
|  |     STATUS_REFUNDED = 3 | ||||||
|  |     STATUS_VOID = 4 | ||||||
|  | 
 | ||||||
|  |     STATUS_TYPES = [ | ||||||
|  |         (STATUS_UNPAID, _("Unpaid")), | ||||||
|  |         (STATUS_PAID, _("Paid")), | ||||||
|  |         (STATUS_REFUNDED, _("Refunded")), | ||||||
|  |         (STATUS_VOID, _("VOID")), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return "Invoice #%d" % self.id |         return "Invoice #%d" % self.id | ||||||
| 
 | 
 | ||||||
|  | @ -541,13 +564,37 @@ class Invoice(models.Model): | ||||||
|             raise ValidationError( |             raise ValidationError( | ||||||
|                 "If this is a cart invoice, it must have a revision") |                 "If this is a cart invoice, it must have a revision") | ||||||
| 
 | 
 | ||||||
|  |     @property | ||||||
|  |     def is_unpaid(self): | ||||||
|  |         return self.status == self.STATUS_UNPAID | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def is_void(self): | ||||||
|  |         return self.status == self.STATUS_VOID | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def is_paid(self): | ||||||
|  |         return self.status == self.STATUS_PAID | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def is_refunded(self): | ||||||
|  |         return self.status == self.STATUS_REFUNDED | ||||||
|  | 
 | ||||||
|     # Invoice Number |     # Invoice Number | ||||||
|     user = models.ForeignKey(User) |     user = models.ForeignKey(User) | ||||||
|     cart = models.ForeignKey(Cart, null=True) |     cart = models.ForeignKey(Cart, null=True) | ||||||
|     cart_revision = models.IntegerField(null=True) |     cart_revision = models.IntegerField( | ||||||
|  |         null=True, | ||||||
|  |         db_index=True, | ||||||
|  |     ) | ||||||
|     # Line Items (foreign key) |     # Line Items (foreign key) | ||||||
|     void = models.BooleanField(default=False) |     status = models.IntegerField( | ||||||
|     paid = models.BooleanField(default=False) |         choices=STATUS_TYPES, | ||||||
|  |         db_index=True, | ||||||
|  |     ) | ||||||
|  |     recipient = models.CharField(max_length=1024) | ||||||
|  |     issue_time = models.DateTimeField() | ||||||
|  |     due_time = models.DateTimeField() | ||||||
|     value = models.DecimalField(max_digits=8, decimal_places=2) |     value = models.DecimalField(max_digits=8, decimal_places=2) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -565,17 +612,25 @@ class LineItem(models.Model): | ||||||
|     description = models.CharField(max_length=255) |     description = models.CharField(max_length=255) | ||||||
|     quantity = models.PositiveIntegerField() |     quantity = models.PositiveIntegerField() | ||||||
|     price = models.DecimalField(max_digits=8, decimal_places=2) |     price = models.DecimalField(max_digits=8, decimal_places=2) | ||||||
|  |     product = models.ForeignKey(Product, null=True, blank=True) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @python_2_unicode_compatible | @python_2_unicode_compatible | ||||||
| class Payment(models.Model): | class PaymentBase(models.Model): | ||||||
|     ''' A payment for an invoice. Each invoice can have multiple payments |     ''' The base payment type for invoices. Payment apps should subclass this | ||||||
|     attached to it.''' |     class to handle implementation-specific issues. ''' | ||||||
|  | 
 | ||||||
|  |     objects = InheritanceManager() | ||||||
| 
 | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return "Payment: ref=%s amount=%s" % (self.reference, self.amount) |         return "Payment: ref=%s amount=%s" % (self.reference, self.amount) | ||||||
| 
 | 
 | ||||||
|     invoice = models.ForeignKey(Invoice) |     invoice = models.ForeignKey(Invoice) | ||||||
|     time = models.DateTimeField(default=timezone.now) |     time = models.DateTimeField(default=timezone.now) | ||||||
|     reference = models.CharField(max_length=64) |     reference = models.CharField(max_length=255) | ||||||
|     amount = models.DecimalField(max_digits=8, decimal_places=2) |     amount = models.DecimalField(max_digits=8, decimal_places=2) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ManualPayment(PaymentBase): | ||||||
|  |     ''' Payments that are manually entered by staff. ''' | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ from registrasion.controllers.invoice import InvoiceController | ||||||
| from registrasion import models as rego | from registrasion import models as rego | ||||||
| 
 | 
 | ||||||
| from django.core.exceptions import ObjectDoesNotExist | from django.core.exceptions import ObjectDoesNotExist | ||||||
|  | from django.core.exceptions import ValidationError | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestingCartController(CartController): | class TestingCartController(CartController): | ||||||
|  | @ -32,4 +33,27 @@ class TestingCartController(CartController): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestingInvoiceController(InvoiceController): | class TestingInvoiceController(InvoiceController): | ||||||
|     pass | 
 | ||||||
|  |     def pay(self, reference, amount): | ||||||
|  |         ''' Testing method for simulating an invoice paymenht by the given | ||||||
|  |         amount. ''' | ||||||
|  |         if self.invoice.cart: | ||||||
|  |             cart = CartController(self.invoice.cart) | ||||||
|  |             cart.validate_cart()  # Raises ValidationError if invalid | ||||||
|  | 
 | ||||||
|  |         status = self.invoice.status | ||||||
|  |         if status == rego.Invoice.STATUS_VOID: | ||||||
|  |             raise ValidationError("Void invoices cannot be paid") | ||||||
|  |         elif status == rego.Invoice.STATUS_PAID: | ||||||
|  |             raise ValidationError("Paid invoices cannot be paid again") | ||||||
|  |         elif status == rego.Invoice.STATUS_REFUNDED: | ||||||
|  |             raise ValidationError("Refunded invoices cannot be paid") | ||||||
|  | 
 | ||||||
|  |         ''' Adds a payment ''' | ||||||
|  |         payment = rego.ManualPayment.objects.create( | ||||||
|  |             invoice=self.invoice, | ||||||
|  |             reference=reference, | ||||||
|  |             amount=amount, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         self.update_status() | ||||||
|  |  | ||||||
|  | @ -35,8 +35,8 @@ class InvoiceTestCase(RegistrationCartTestCase): | ||||||
|         # The old invoice should automatically be voided |         # The old invoice should automatically be voided | ||||||
|         invoice_1_new = rego.Invoice.objects.get(pk=invoice_1.invoice.id) |         invoice_1_new = rego.Invoice.objects.get(pk=invoice_1.invoice.id) | ||||||
|         invoice_2_new = rego.Invoice.objects.get(pk=invoice_2.invoice.id) |         invoice_2_new = rego.Invoice.objects.get(pk=invoice_2.invoice.id) | ||||||
|         self.assertTrue(invoice_1_new.void) |         self.assertTrue(invoice_1_new.is_void) | ||||||
|         self.assertFalse(invoice_2_new.void) |         self.assertFalse(invoice_2_new.is_void) | ||||||
| 
 | 
 | ||||||
|         # Invoice should have two line items |         # Invoice should have two line items | ||||||
|         line_items = rego.LineItem.objects.filter(invoice=invoice_2.invoice) |         line_items = rego.LineItem.objects.filter(invoice=invoice_2.invoice) | ||||||
|  | @ -68,7 +68,7 @@ class InvoiceTestCase(RegistrationCartTestCase): | ||||||
|         invoice.pay("A payment!", invoice.invoice.value) |         invoice.pay("A payment!", invoice.invoice.value) | ||||||
| 
 | 
 | ||||||
|         # This payment is for the correct amount invoice should be paid. |         # This payment is for the correct amount invoice should be paid. | ||||||
|         self.assertTrue(invoice.invoice.paid) |         self.assertTrue(invoice.invoice.is_paid) | ||||||
| 
 | 
 | ||||||
|         # Cart should not be active |         # Cart should not be active | ||||||
|         self.assertFalse(invoice.invoice.cart.active) |         self.assertFalse(invoice.invoice.cart.active) | ||||||
|  | @ -133,7 +133,7 @@ class InvoiceTestCase(RegistrationCartTestCase): | ||||||
|         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_1 = TestingInvoiceController.for_cart(current_cart.cart) | ||||||
| 
 | 
 | ||||||
|         self.assertTrue(invoice_1.invoice.paid) |         self.assertTrue(invoice_1.invoice.is_paid) | ||||||
| 
 | 
 | ||||||
|     def test_invoice_voids_self_if_cart_is_invalid(self): |     def test_invoice_voids_self_if_cart_is_invalid(self): | ||||||
|         current_cart = TestingCartController.for_user(self.USER_1) |         current_cart = TestingCartController.for_user(self.USER_1) | ||||||
|  | @ -142,7 +142,7 @@ class InvoiceTestCase(RegistrationCartTestCase): | ||||||
|         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_1 = TestingInvoiceController.for_cart(current_cart.cart) | ||||||
| 
 | 
 | ||||||
|         self.assertFalse(invoice_1.invoice.void) |         self.assertFalse(invoice_1.invoice.is_void) | ||||||
| 
 | 
 | ||||||
|         # Adding item to cart should produce a new invoice |         # Adding item to cart should produce a new invoice | ||||||
|         current_cart.add_to_cart(self.PROD_2, 1) |         current_cart.add_to_cart(self.PROD_2, 1) | ||||||
|  | @ -151,11 +151,11 @@ class InvoiceTestCase(RegistrationCartTestCase): | ||||||
| 
 | 
 | ||||||
|         # Viewing invoice_1's invoice should show it as void |         # Viewing invoice_1's invoice should show it as void | ||||||
|         invoice_1_new = TestingInvoiceController(invoice_1.invoice) |         invoice_1_new = TestingInvoiceController(invoice_1.invoice) | ||||||
|         self.assertTrue(invoice_1_new.invoice.void) |         self.assertTrue(invoice_1_new.invoice.is_void) | ||||||
| 
 | 
 | ||||||
|         # Viewing invoice_2's invoice should *not* show it as void |         # Viewing invoice_2's invoice should *not* show it as void | ||||||
|         invoice_2_new = TestingInvoiceController(invoice_2.invoice) |         invoice_2_new = TestingInvoiceController(invoice_2.invoice) | ||||||
|         self.assertFalse(invoice_2_new.invoice.void) |         self.assertFalse(invoice_2_new.invoice.is_void) | ||||||
| 
 | 
 | ||||||
|     def test_voiding_invoice_creates_new_invoice(self): |     def test_voiding_invoice_creates_new_invoice(self): | ||||||
|         current_cart = TestingCartController.for_user(self.USER_1) |         current_cart = TestingCartController.for_user(self.USER_1) | ||||||
|  | @ -164,7 +164,7 @@ class InvoiceTestCase(RegistrationCartTestCase): | ||||||
|         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_1 = TestingInvoiceController.for_cart(current_cart.cart) | ||||||
| 
 | 
 | ||||||
|         self.assertFalse(invoice_1.invoice.void) |         self.assertFalse(invoice_1.invoice.is_void) | ||||||
|         invoice_1.void() |         invoice_1.void() | ||||||
| 
 | 
 | ||||||
|         invoice_2 = TestingInvoiceController.for_cart(current_cart.cart) |         invoice_2 = TestingInvoiceController.for_cart(current_cart.cart) | ||||||
|  |  | ||||||
|  | @ -18,11 +18,13 @@ class RefundTestCase(RegistrationCartTestCase): | ||||||
|         invoice = TestingInvoiceController.for_cart(current_cart.cart) |         invoice = TestingInvoiceController.for_cart(current_cart.cart) | ||||||
| 
 | 
 | ||||||
|         invoice.pay("A Payment!", invoice.invoice.value) |         invoice.pay("A Payment!", invoice.invoice.value) | ||||||
|         self.assertFalse(invoice.invoice.void) |         self.assertFalse(invoice.invoice.is_void) | ||||||
|         self.assertTrue(invoice.invoice.paid) |         self.assertTrue(invoice.invoice.is_paid) | ||||||
|  |         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("A Refund!", invoice.invoice.value) | ||||||
|         self.assertTrue(invoice.invoice.void) |         self.assertFalse(invoice.invoice.is_void) | ||||||
|         self.assertFalse(invoice.invoice.paid) |         self.assertFalse(invoice.invoice.is_paid) | ||||||
|  |         self.assertTrue(invoice.invoice.is_refunded) | ||||||
|         self.assertTrue(invoice.invoice.cart.released) |         self.assertTrue(invoice.invoice.cart.released) | ||||||
|  |  | ||||||
|  | @ -141,7 +141,7 @@ class VoucherTestCases(RegistrationCartTestCase): | ||||||
|         current_cart.add_to_cart(self.PROD_1, 1) |         current_cart.add_to_cart(self.PROD_1, 1) | ||||||
| 
 | 
 | ||||||
|         inv = TestingInvoiceController.for_cart(current_cart.cart) |         inv = TestingInvoiceController.for_cart(current_cart.cart) | ||||||
|         if not inv.invoice.paid: |         if not inv.invoice.is_paid: | ||||||
|             inv.pay("Hello!", inv.invoice.value) |             inv.pay("Hello!", inv.invoice.value) | ||||||
| 
 | 
 | ||||||
|         current_cart = TestingCartController.for_user(self.USER_1) |         current_cart = TestingCartController.for_user(self.USER_1) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer