Merge branch 'invoices_and_payments'
This commit is contained in:
		
						commit
						a12460e351
					
				
					 18 changed files with 578 additions and 141 deletions
				
			
		|  | @ -3,6 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist | |||
| from django.core.exceptions import ValidationError | ||||
| from django.db import transaction | ||||
| from django.db.models import Sum | ||||
| from django.utils import timezone | ||||
| 
 | ||||
| from registrasion import models as rego | ||||
| 
 | ||||
|  | @ -13,6 +14,7 @@ class InvoiceController(object): | |||
| 
 | ||||
|     def __init__(self, invoice): | ||||
|         self.invoice = invoice | ||||
|         self.update_status() | ||||
|         self.update_validity()  # Make sure this invoice is up-to-date | ||||
| 
 | ||||
|     @classmethod | ||||
|  | @ -22,21 +24,26 @@ class InvoiceController(object): | |||
|         an invoice is generated.''' | ||||
| 
 | ||||
|         try: | ||||
|             invoice = rego.Invoice.objects.get( | ||||
|             invoice = rego.Invoice.objects.exclude( | ||||
|                 status=rego.Invoice.STATUS_VOID, | ||||
|             ).get( | ||||
|                 cart=cart, | ||||
|                 cart_revision=cart.revision, | ||||
|                 void=False, | ||||
|             ) | ||||
|         except ObjectDoesNotExist: | ||||
|             cart_controller = CartController(cart) | ||||
|             cart_controller.validate_cart()  # Raises ValidationError on fail. | ||||
| 
 | ||||
|             # Void past invoices for this cart | ||||
|             rego.Invoice.objects.filter(cart=cart).update(void=True) | ||||
| 
 | ||||
|             cls.void_all_invoices(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 | ||||
|     def resolve_discount_value(cls, item): | ||||
|  | @ -60,11 +67,26 @@ class InvoiceController(object): | |||
|     @transaction.atomic | ||||
|     def _generate(cls, 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) | ||||
| 
 | ||||
|         # Get the invoice recipient | ||||
|         profile = rego.AttendeeProfileBase.objects.get_subclass( | ||||
|             id=cart.user.attendee.attendeeprofilebase.id, | ||||
|         ) | ||||
|         recipient = profile.invoice_recipient() | ||||
|         invoice = rego.Invoice.objects.create( | ||||
|             user=cart.user, | ||||
|             cart=cart, | ||||
|             cart_revision=cart.revision, | ||||
|             value=Decimal() | ||||
|             status=rego.Invoice.STATUS_UNPAID, | ||||
|             value=Decimal(), | ||||
|             issue_time=issued, | ||||
|             due_time=due, | ||||
|             recipient=recipient, | ||||
|         ) | ||||
| 
 | ||||
|         product_items = rego.ProductItem.objects.filter(cart=cart) | ||||
|  | @ -84,6 +106,7 @@ class InvoiceController(object): | |||
|                 description="%s - %s" % (product.category.name, product.name), | ||||
|                 quantity=item.quantity, | ||||
|                 price=product.price, | ||||
|                 product=product, | ||||
|             ) | ||||
|             invoice_value += line_item.quantity * line_item.price | ||||
| 
 | ||||
|  | @ -93,94 +116,162 @@ class InvoiceController(object): | |||
|                 description=item.discount.description, | ||||
|                 quantity=item.quantity, | ||||
|                 price=cls.resolve_discount_value(item) * -1, | ||||
|                 product=item.product, | ||||
|             ) | ||||
|             invoice_value += line_item.quantity * line_item.price | ||||
| 
 | ||||
|         invoice.value = invoice_value | ||||
| 
 | ||||
|         if invoice.value == 0: | ||||
|             invoice.paid = True | ||||
| 
 | ||||
|         invoice.save() | ||||
| 
 | ||||
|         return invoice | ||||
| 
 | ||||
|     def can_view(self, user=None, access_code=None): | ||||
|         ''' Returns true if the accessing user is allowed to view this invoice, | ||||
|         or if the given access code matches this invoice's user's access code. | ||||
|         ''' | ||||
| 
 | ||||
|         if user == self.invoice.user: | ||||
|             return True | ||||
| 
 | ||||
|         if user.is_staff: | ||||
|             return True | ||||
| 
 | ||||
|         if self.invoice.user.attendee.access_code == access_code: | ||||
|             return True | ||||
| 
 | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
|     def _refresh(self): | ||||
|         ''' Refreshes the underlying invoice and cart objects. ''' | ||||
|         self.invoice.refresh_from_db() | ||||
|         if self.invoice.cart: | ||||
|             self.invoice.cart.refresh_from_db() | ||||
| 
 | ||||
|     def validate_allowed_to_pay(self): | ||||
|         ''' Passes cleanly if we're allowed to pay, otherwise raise | ||||
|         a ValidationError. ''' | ||||
| 
 | ||||
|         self._refresh() | ||||
| 
 | ||||
|         if not self.invoice.is_unpaid: | ||||
|             raise ValidationError("You can only pay for unpaid invoices.") | ||||
| 
 | ||||
|         if not self.invoice.cart: | ||||
|             return | ||||
| 
 | ||||
|         if not self._invoice_matches_cart(): | ||||
|             raise ValidationError("The registration has been amended since " | ||||
|                                   "generating this invoice.") | ||||
| 
 | ||||
|         CartController(self.invoice.cart).validate_cart() | ||||
| 
 | ||||
|     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): | ||||
|         ''' Updates the validity of this invoice if the cart it is attached to | ||||
|         has updated. ''' | ||||
|         if self.invoice.cart is not None: | ||||
|             if self.invoice.cart.revision != self.invoice.cart_revision: | ||||
|                 self.void() | ||||
|         ''' Voids this invoice if the cart it is attached to has updated. ''' | ||||
|         if not self._invoice_matches_cart(): | ||||
|             self.void() | ||||
| 
 | ||||
|     def void(self): | ||||
|         ''' 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, " | ||||
|                                   "only refunded.") | ||||
|         self.invoice.void = True | ||||
|         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() | ||||
|         self._mark_void() | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def refund(self, reference, amount): | ||||
|         ''' Refunds the invoice by the given amount. The invoice is | ||||
|         marked as unpaid, and the underlying cart is marked as released. | ||||
|         ''' Refunds the invoice by the given amount. | ||||
| 
 | ||||
|         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") | ||||
| 
 | ||||
|         ''' Adds a payment ''' | ||||
|         payment = rego.Payment.objects.create( | ||||
|         # Adds a payment | ||||
|         # TODO: replace by creating a credit note instead | ||||
|         rego.ManualPayment.objects.create( | ||||
|             invoice=self.invoice, | ||||
|             reference=reference, | ||||
|             amount=0 - amount, | ||||
|         ) | ||||
|         payment.save() | ||||
| 
 | ||||
|         self.invoice.paid = False | ||||
|         self.invoice.void = True | ||||
| 
 | ||||
|         if self.invoice.cart: | ||||
|             cart = self.invoice.cart | ||||
|             cart.released = True | ||||
|             cart.save() | ||||
| 
 | ||||
|         self.invoice.save() | ||||
|         self.update_status() | ||||
|  |  | |||
|  | @ -3,6 +3,13 @@ import models as rego | |||
| from django import forms | ||||
| 
 | ||||
| 
 | ||||
| class ManualPaymentForm(forms.ModelForm): | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = rego.ManualPayment | ||||
|         fields = ["reference", "amount"] | ||||
| 
 | ||||
| 
 | ||||
| # Products forms -- none of these have any fields: they are to be subclassed | ||||
| # and the fields added as needs be. | ||||
| 
 | ||||
|  |  | |||
|  | @ -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), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										21
									
								
								registrasion/migrations/0014_attendee_access_code.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								registrasion/migrations/0014_attendee_access_code.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9.2 on 2016-04-08 02:20 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import registrasion.util | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('registrasion', '0013_auto_20160406_2228_squashed_0015_auto_20160406_1942'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='attendee', | ||||
|             name='access_code', | ||||
|             field=models.CharField(default=registrasion.util.generate_access_code, max_length=6, unique=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										20
									
								
								registrasion/migrations/0015_auto_20160408_0220.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								registrasion/migrations/0015_auto_20160408_0220.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9.2 on 2016-04-08 02:20 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('registrasion', '0014_attendee_access_code'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='attendee', | ||||
|             name='access_code', | ||||
|             field=models.CharField(max_length=6, unique=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										20
									
								
								registrasion/migrations/0016_auto_20160408_0234.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								registrasion/migrations/0016_auto_20160408_0234.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9.2 on 2016-04-08 02:34 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('registrasion', '0015_auto_20160408_0220'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='attendee', | ||||
|             name='access_code', | ||||
|             field=models.CharField(db_index=True, max_length=6, unique=True), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -1,8 +1,11 @@ | |||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import util | ||||
| 
 | ||||
| import datetime | ||||
| import itertools | ||||
| 
 | ||||
| from django.core.exceptions import ObjectDoesNotExist | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.contrib.auth.models import User | ||||
| from django.db import models | ||||
|  | @ -26,16 +29,25 @@ class Attendee(models.Model): | |||
|     def get_instance(user): | ||||
|         ''' Returns the instance of attendee for the given user, or creates | ||||
|         a new one. ''' | ||||
|         attendees = Attendee.objects.filter(user=user) | ||||
|         if len(attendees) > 0: | ||||
|             return attendees[0] | ||||
|         else: | ||||
|             attendee = Attendee(user=user) | ||||
|             attendee.save() | ||||
|             return attendee | ||||
|         try: | ||||
|             return Attendee.objects.get(user=user) | ||||
|         except ObjectDoesNotExist: | ||||
|             return Attendee.objects.create(user=user) | ||||
| 
 | ||||
|     def save(self, *a, **k): | ||||
|         while not self.access_code: | ||||
|             access_code = util.generate_access_code() | ||||
|             if Attendee.objects.filter(access_code=access_code).count() == 0: | ||||
|                 self.access_code = access_code | ||||
|         return super(Attendee, self).save(*a, **k) | ||||
| 
 | ||||
|     user = models.OneToOneField(User, on_delete=models.CASCADE) | ||||
|     # Badge/profile is linked | ||||
|     access_code = models.CharField( | ||||
|         max_length=6, | ||||
|         unique=True, | ||||
|         db_index=True, | ||||
|     ) | ||||
|     completed_registration = models.BooleanField(default=False) | ||||
|     highest_complete_category = models.IntegerField(default=0) | ||||
| 
 | ||||
|  | @ -54,6 +66,19 @@ class AttendeeProfileBase(models.Model): | |||
|         speaker profile. If it's None, that functionality is disabled. ''' | ||||
|         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) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -533,6 +558,18 @@ class Invoice(models.Model): | |||
|     ''' 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. ''' | ||||
| 
 | ||||
|     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): | ||||
|         return "Invoice #%d" % self.id | ||||
| 
 | ||||
|  | @ -541,13 +578,37 @@ class Invoice(models.Model): | |||
|             raise ValidationError( | ||||
|                 "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 | ||||
|     user = models.ForeignKey(User) | ||||
|     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) | ||||
|     void = models.BooleanField(default=False) | ||||
|     paid = models.BooleanField(default=False) | ||||
|     status = models.IntegerField( | ||||
|         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) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -565,17 +626,25 @@ class LineItem(models.Model): | |||
|     description = models.CharField(max_length=255) | ||||
|     quantity = models.PositiveIntegerField() | ||||
|     price = models.DecimalField(max_digits=8, decimal_places=2) | ||||
|     product = models.ForeignKey(Product, null=True, blank=True) | ||||
| 
 | ||||
| 
 | ||||
| @python_2_unicode_compatible | ||||
| class Payment(models.Model): | ||||
|     ''' A payment for an invoice. Each invoice can have multiple payments | ||||
|     attached to it.''' | ||||
| class PaymentBase(models.Model): | ||||
|     ''' The base payment type for invoices. Payment apps should subclass this | ||||
|     class to handle implementation-specific issues. ''' | ||||
| 
 | ||||
|     objects = InheritanceManager() | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return "Payment: ref=%s amount=%s" % (self.reference, self.amount) | ||||
| 
 | ||||
|     invoice = models.ForeignKey(Invoice) | ||||
|     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) | ||||
| 
 | ||||
| 
 | ||||
| class ManualPayment(PaymentBase): | ||||
|     ''' Payments that are manually entered by staff. ''' | ||||
|     pass | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| from registrasion.controllers.cart import CartController | ||||
| from registrasion.controllers.invoice import InvoiceController | ||||
| from registrasion import models as rego | ||||
| 
 | ||||
| from django.core.exceptions import ObjectDoesNotExist | ||||
| from django.core.exceptions import ValidationError | ||||
| 
 | ||||
| 
 | ||||
| class TestingCartController(CartController): | ||||
|  | @ -28,3 +30,21 @@ class TestingCartController(CartController): | |||
|     def next_cart(self): | ||||
|         self.cart.active = False | ||||
|         self.cart.save() | ||||
| 
 | ||||
| 
 | ||||
| class TestingInvoiceController(InvoiceController): | ||||
| 
 | ||||
|     def pay(self, reference, amount): | ||||
|         ''' Testing method for simulating an invoice paymenht by the given | ||||
|         amount. ''' | ||||
| 
 | ||||
|         self.validate_allowed_to_pay() | ||||
| 
 | ||||
|         ''' Adds a payment ''' | ||||
|         payment = rego.ManualPayment.objects.create( | ||||
|             invoice=self.invoice, | ||||
|             reference=reference, | ||||
|             amount=amount, | ||||
|         ) | ||||
| 
 | ||||
|         self.update_status() | ||||
|  | @ -10,7 +10,7 @@ from django.test import TestCase | |||
| from registrasion import models as rego | ||||
| from registrasion.controllers.product import ProductController | ||||
| 
 | ||||
| from cart_controller_helper import TestingCartController | ||||
| from controller_helpers import TestingCartController | ||||
| from patch_datetime import SetTimeMixin | ||||
| 
 | ||||
| UTC = pytz.timezone('UTC') | ||||
|  | @ -23,6 +23,9 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase): | |||
| 
 | ||||
|     @classmethod | ||||
|     def setUpTestData(cls): | ||||
| 
 | ||||
|         super(RegistrationCartTestCase, cls).setUpTestData() | ||||
| 
 | ||||
|         cls.USER_1 = User.objects.create_user( | ||||
|             username='testuser', | ||||
|             email='test@example.com', | ||||
|  | @ -33,6 +36,15 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase): | |||
|             email='test2@example.com', | ||||
|             password='top_secret') | ||||
| 
 | ||||
|         attendee1 = rego.Attendee.get_instance(cls.USER_1) | ||||
|         attendee1.save() | ||||
|         profile1 = rego.AttendeeProfileBase.objects.create(attendee=attendee1) | ||||
|         profile1.save() | ||||
|         attendee2 = rego.Attendee.get_instance(cls.USER_2) | ||||
|         attendee2.save() | ||||
|         profile2 = rego.AttendeeProfileBase.objects.create(attendee=attendee2) | ||||
|         profile2.save() | ||||
| 
 | ||||
|         cls.RESERVATION = datetime.timedelta(hours=1) | ||||
| 
 | ||||
|         cls.categories = [] | ||||
|  | @ -136,6 +148,10 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase): | |||
|         voucher.save() | ||||
|         return voucher | ||||
| 
 | ||||
|     @classmethod | ||||
|     def reget(cls, object): | ||||
|         return type(object).objects.get(id=object.id) | ||||
| 
 | ||||
| 
 | ||||
| class BasicCartTests(RegistrationCartTestCase): | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import pytz | |||
| 
 | ||||
| from django.core.exceptions import ValidationError | ||||
| 
 | ||||
| from cart_controller_helper import TestingCartController | ||||
| from controller_helpers import TestingCartController | ||||
| from test_cart import RegistrationCartTestCase | ||||
| 
 | ||||
| from registrasion import models as rego | ||||
|  |  | |||
|  | @ -4,7 +4,8 @@ from decimal import Decimal | |||
| 
 | ||||
| from registrasion import models as rego | ||||
| from registrasion.controllers import discount | ||||
| from cart_controller_helper import TestingCartController | ||||
| from controller_helpers import TestingCartController | ||||
| from controller_helpers import TestingInvoiceController | ||||
| 
 | ||||
| from test_cart import RegistrationCartTestCase | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ from django.core.exceptions import ValidationError | |||
| 
 | ||||
| from registrasion import models as rego | ||||
| from registrasion.controllers.category import CategoryController | ||||
| from cart_controller_helper import TestingCartController | ||||
| from controller_helpers import TestingCartController | ||||
| from registrasion.controllers.product import ProductController | ||||
| 
 | ||||
| from test_cart import RegistrationCartTestCase | ||||
|  |  | |||
|  | @ -5,8 +5,8 @@ from decimal import Decimal | |||
| from django.core.exceptions import ValidationError | ||||
| 
 | ||||
| from registrasion import models as rego | ||||
| from cart_controller_helper import TestingCartController | ||||
| from registrasion.controllers.invoice import InvoiceController | ||||
| from controller_helpers import TestingCartController | ||||
| from controller_helpers import TestingInvoiceController | ||||
| 
 | ||||
| from test_cart import RegistrationCartTestCase | ||||
| 
 | ||||
|  | @ -20,7 +20,7 @@ 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 = InvoiceController.for_cart(current_cart.cart) | ||||
|         invoice_1 = TestingInvoiceController.for_cart(current_cart.cart) | ||||
|         # That invoice should have a single line item | ||||
|         line_items = rego.LineItem.objects.filter(invoice=invoice_1.invoice) | ||||
|         self.assertEqual(1, len(line_items)) | ||||
|  | @ -29,14 +29,14 @@ class InvoiceTestCase(RegistrationCartTestCase): | |||
| 
 | ||||
|         # Adding item to cart should produce a new invoice | ||||
|         current_cart.add_to_cart(self.PROD_2, 1) | ||||
|         invoice_2 = InvoiceController.for_cart(current_cart.cart) | ||||
|         invoice_2 = TestingInvoiceController.for_cart(current_cart.cart) | ||||
|         self.assertNotEqual(invoice_1.invoice, invoice_2.invoice) | ||||
| 
 | ||||
|         # The old invoice should automatically be voided | ||||
|         invoice_1_new = rego.Invoice.objects.get(pk=invoice_1.invoice.id) | ||||
|         invoice_2_new = rego.Invoice.objects.get(pk=invoice_2.invoice.id) | ||||
|         self.assertTrue(invoice_1_new.void) | ||||
|         self.assertFalse(invoice_2_new.void) | ||||
|         self.assertTrue(invoice_1_new.is_void) | ||||
|         self.assertFalse(invoice_2_new.is_void) | ||||
| 
 | ||||
|         # Invoice should have two line items | ||||
|         line_items = rego.LineItem.objects.filter(invoice=invoice_2.invoice) | ||||
|  | @ -58,17 +58,17 @@ class InvoiceTestCase(RegistrationCartTestCase): | |||
| 
 | ||||
|         # Now try to invoice the first user | ||||
|         with self.assertRaises(ValidationError): | ||||
|             InvoiceController.for_cart(current_cart.cart) | ||||
|             TestingInvoiceController.for_cart(current_cart.cart) | ||||
| 
 | ||||
|     def test_paying_invoice_makes_new_cart(self): | ||||
|         current_cart = TestingCartController.for_user(self.USER_1) | ||||
|         current_cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         invoice = InvoiceController.for_cart(current_cart.cart) | ||||
|         invoice = TestingInvoiceController.for_cart(current_cart.cart) | ||||
|         invoice.pay("A payment!", invoice.invoice.value) | ||||
| 
 | ||||
|         # 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 | ||||
|         self.assertFalse(invoice.invoice.cart.active) | ||||
|  | @ -99,7 +99,7 @@ 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 = InvoiceController.for_cart(current_cart.cart) | ||||
|         invoice_1 = TestingInvoiceController.for_cart(current_cart.cart) | ||||
| 
 | ||||
|         # That invoice should have two line items | ||||
|         line_items = rego.LineItem.objects.filter(invoice=invoice_1.invoice) | ||||
|  | @ -131,43 +131,43 @@ 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 = InvoiceController.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): | ||||
|         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_1 = InvoiceController.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 | ||||
|         current_cart.add_to_cart(self.PROD_2, 1) | ||||
|         invoice_2 = InvoiceController.for_cart(current_cart.cart) | ||||
|         invoice_2 = TestingInvoiceController.for_cart(current_cart.cart) | ||||
|         self.assertNotEqual(invoice_1.invoice, invoice_2.invoice) | ||||
| 
 | ||||
|         # Viewing invoice_1's invoice should show it as void | ||||
|         invoice_1_new = InvoiceController(invoice_1.invoice) | ||||
|         self.assertTrue(invoice_1_new.invoice.void) | ||||
|         invoice_1_new = TestingInvoiceController(invoice_1.invoice) | ||||
|         self.assertTrue(invoice_1_new.invoice.is_void) | ||||
| 
 | ||||
|         # Viewing invoice_2's invoice should *not* show it as void | ||||
|         invoice_2_new = InvoiceController(invoice_2.invoice) | ||||
|         self.assertFalse(invoice_2_new.invoice.void) | ||||
|         invoice_2_new = TestingInvoiceController(invoice_2.invoice) | ||||
|         self.assertFalse(invoice_2_new.invoice.is_void) | ||||
| 
 | ||||
|     def test_voiding_invoice_creates_new_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_1 = InvoiceController.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_2 = InvoiceController.for_cart(current_cart.cart) | ||||
|         invoice_2 = TestingInvoiceController.for_cart(current_cart.cart) | ||||
|         self.assertNotEqual(invoice_1.invoice, invoice_2.invoice) | ||||
| 
 | ||||
|     def test_cannot_pay_void_invoice(self): | ||||
|  | @ -175,19 +175,19 @@ 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 = InvoiceController.for_cart(current_cart.cart) | ||||
|         invoice_1 = TestingInvoiceController.for_cart(current_cart.cart) | ||||
| 
 | ||||
|         invoice_1.void() | ||||
| 
 | ||||
|         with self.assertRaises(ValidationError): | ||||
|             invoice_1.pay("Reference", invoice_1.invoice.value) | ||||
|             invoice_1.validate_allowed_to_pay() | ||||
| 
 | ||||
|     def test_cannot_void_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_1 = InvoiceController.for_cart(current_cart.cart) | ||||
|         invoice_1 = TestingInvoiceController.for_cart(current_cart.cart) | ||||
| 
 | ||||
|         invoice_1.pay("Reference", invoice_1.invoice.value) | ||||
| 
 | ||||
|  | @ -197,4 +197,24 @@ class InvoiceTestCase(RegistrationCartTestCase): | |||
|     def test_cannot_generate_blank_invoice(self): | ||||
|         current_cart = TestingCartController.for_user(self.USER_1) | ||||
|         with self.assertRaises(ValidationError): | ||||
|             InvoiceController.for_cart(current_cart.cart) | ||||
|             invoice_1 = TestingInvoiceController.for_cart(current_cart.cart) | ||||
| 
 | ||||
|     def test_cannot_pay_implicitly_void_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)) | ||||
| 
 | ||||
|         # Implicitly void the invoice | ||||
|         cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         with self.assertRaises(ValidationError): | ||||
|             invoice.validate_allowed_to_pay() | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     # TODO: test partially paid invoice cannot be void until payments | ||||
|     # are refunded | ||||
| 
 | ||||
|     # TODO: test overpaid invoice results in credit note | ||||
| 
 | ||||
|     # TODO: test credit note generation more generally | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import pytz | ||||
| 
 | ||||
| from cart_controller_helper import TestingCartController | ||||
| from registrasion.controllers.invoice import InvoiceController | ||||
| from controller_helpers import TestingCartController | ||||
| from controller_helpers import TestingInvoiceController | ||||
| 
 | ||||
| from test_cart import RegistrationCartTestCase | ||||
| 
 | ||||
|  | @ -15,14 +15,16 @@ class RefundTestCase(RegistrationCartTestCase): | |||
| 
 | ||||
|         # Should be able to create an invoice after the product is added | ||||
|         current_cart.add_to_cart(self.PROD_1, 1) | ||||
|         invoice = InvoiceController.for_cart(current_cart.cart) | ||||
|         invoice = TestingInvoiceController.for_cart(current_cart.cart) | ||||
| 
 | ||||
|         invoice.pay("A Payment!", invoice.invoice.value) | ||||
|         self.assertFalse(invoice.invoice.void) | ||||
|         self.assertTrue(invoice.invoice.paid) | ||||
|         self.assertFalse(invoice.invoice.is_void) | ||||
|         self.assertTrue(invoice.invoice.is_paid) | ||||
|         self.assertFalse(invoice.invoice.is_refunded) | ||||
|         self.assertFalse(invoice.invoice.cart.released) | ||||
| 
 | ||||
|         invoice.refund("A Refund!", invoice.invoice.value) | ||||
|         self.assertTrue(invoice.invoice.void) | ||||
|         self.assertFalse(invoice.invoice.paid) | ||||
|         self.assertFalse(invoice.invoice.is_void) | ||||
|         self.assertFalse(invoice.invoice.is_paid) | ||||
|         self.assertTrue(invoice.invoice.is_refunded) | ||||
|         self.assertTrue(invoice.invoice.cart.released) | ||||
|  |  | |||
|  | @ -6,8 +6,8 @@ from django.core.exceptions import ValidationError | |||
| from django.db import IntegrityError | ||||
| 
 | ||||
| from registrasion import models as rego | ||||
| from cart_controller_helper import TestingCartController | ||||
| from registrasion.controllers.invoice import InvoiceController | ||||
| from controller_helpers import TestingCartController | ||||
| from controller_helpers import TestingInvoiceController | ||||
| 
 | ||||
| from test_cart import RegistrationCartTestCase | ||||
| 
 | ||||
|  | @ -140,8 +140,8 @@ class VoucherTestCases(RegistrationCartTestCase): | |||
|         current_cart.apply_voucher(voucher.code) | ||||
|         current_cart.add_to_cart(self.PROD_1, 1) | ||||
| 
 | ||||
|         inv = InvoiceController.for_cart(current_cart.cart) | ||||
|         if not inv.invoice.paid: | ||||
|         inv = TestingInvoiceController.for_cart(current_cart.cart) | ||||
|         if not inv.invoice.is_paid: | ||||
|             inv.pay("Hello!", inv.invoice.value) | ||||
| 
 | ||||
|         current_cart = TestingCartController.for_user(self.USER_1) | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| import views | ||||
| 
 | ||||
| from django.conf.urls import url, patterns | ||||
| 
 | ||||
| urlpatterns = patterns( | ||||
|  | @ -5,7 +7,11 @@ urlpatterns = patterns( | |||
|     url(r"^category/([0-9]+)$", "product_category", name="product_category"), | ||||
|     url(r"^checkout$", "checkout", name="checkout"), | ||||
|     url(r"^invoice/([0-9]+)$", "invoice", name="invoice"), | ||||
|     url(r"^invoice/([0-9]+)/pay$", "pay_invoice", name="pay_invoice"), | ||||
|     url(r"^invoice/([0-9]+)/([A-Z0-9]+)$", views.invoice, name="invoice"), | ||||
|     url(r"^invoice/([0-9]+)/manual_payment$", | ||||
|         views.manual_payment, name="manual_payment"), | ||||
|     url(r"^invoice_access/([A-Z0-9]+)$", views.invoice_access, | ||||
|         name="invoice_access"), | ||||
|     url(r"^profile$", "edit_profile", name="attendee_edit"), | ||||
|     url(r"^register$", "guided_registration", name="guided_registration"), | ||||
|     url(r"^register/([0-9]+)$", "guided_registration", | ||||
|  |  | |||
							
								
								
									
										15
									
								
								registrasion/util.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								registrasion/util.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| import string | ||||
| 
 | ||||
| from django.utils.crypto import get_random_string | ||||
| 
 | ||||
| def generate_access_code(): | ||||
|     ''' Generates an access code for users' payments as well as their | ||||
|     fulfilment code for check-in. | ||||
|     The access code will 4 characters long, which allows for 1,500,625 | ||||
|     unique codes, which really should be enough for anyone. ''' | ||||
| 
 | ||||
|     length = 4 | ||||
|     # all upper-case letters + digits 1-9 (no 0 vs O confusion) | ||||
|     chars = string.uppercase + string.digits[1:] | ||||
|     # 4 chars => 35 ** 4 = 1500625 (should be enough for anyone) | ||||
|     return get_random_string(length=length, allowed_chars=chars) | ||||
|  | @ -16,6 +16,7 @@ from django.contrib import messages | |||
| from django.core.exceptions import ObjectDoesNotExist | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.http import Http404 | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.shortcuts import redirect | ||||
| from django.shortcuts import render | ||||
| 
 | ||||
|  | @ -423,18 +424,41 @@ def checkout_errors(request, errors): | |||
|     return render(request, "registrasion/checkout_errors.html", data) | ||||
| 
 | ||||
| 
 | ||||
| @login_required | ||||
| def invoice(request, invoice_id): | ||||
|     ''' Displays an invoice for a given invoice id. ''' | ||||
| def invoice_access(request, access_code): | ||||
|     ''' Redirects to the first unpaid invoice for the attendee that matches | ||||
|     the given access code, if any. ''' | ||||
| 
 | ||||
|     invoices = rego.Invoice.objects.filter( | ||||
|         user__attendee__access_code=access_code, | ||||
|         status=rego.Invoice.STATUS_UNPAID, | ||||
|     ).order_by("issue_time") | ||||
| 
 | ||||
|     if not invoices: | ||||
|         raise Http404() | ||||
| 
 | ||||
|     invoice = invoices[0] | ||||
| 
 | ||||
|     return redirect("invoice", invoice.id, access_code) | ||||
| 
 | ||||
| 
 | ||||
| def invoice(request, invoice_id, access_code=None): | ||||
|     ''' Displays an invoice for a given invoice id. | ||||
|     This view is not authenticated, but it will only allow access to either: | ||||
|     the user the invoice belongs to; staff; or a request made with the correct | ||||
|     access code. | ||||
|     ''' | ||||
| 
 | ||||
|     invoice_id = int(invoice_id) | ||||
|     inv = rego.Invoice.objects.get(pk=invoice_id) | ||||
| 
 | ||||
|     if request.user != inv.cart.user and not request.user.is_staff: | ||||
|         raise Http404() | ||||
| 
 | ||||
|     current_invoice = InvoiceController(inv) | ||||
| 
 | ||||
|     if not current_invoice.can_view( | ||||
|             user=request.user, | ||||
|             access_code=access_code, | ||||
|         ): | ||||
|         raise Http404() | ||||
| 
 | ||||
|     data = { | ||||
|         "invoice": current_invoice.invoice, | ||||
|     } | ||||
|  | @ -443,15 +467,32 @@ def invoice(request, invoice_id): | |||
| 
 | ||||
| 
 | ||||
| @login_required | ||||
| def pay_invoice(request, invoice_id): | ||||
|     ''' Marks the invoice with the given invoice id as paid. | ||||
|     WORK IN PROGRESS FUNCTION. Must be replaced with real payment workflow. | ||||
| def manual_payment(request, invoice_id): | ||||
|     ''' Allows staff to make manual payments or refunds on an invoice.''' | ||||
| 
 | ||||
|     FORM_PREFIX = "manual_payment" | ||||
| 
 | ||||
|     if not request.user.is_staff: | ||||
|         raise Http404() | ||||
| 
 | ||||
|     ''' | ||||
|     invoice_id = int(invoice_id) | ||||
|     inv = rego.Invoice.objects.get(pk=invoice_id) | ||||
|     inv = get_object_or_404(rego.Invoice, pk=invoice_id) | ||||
|     current_invoice = InvoiceController(inv) | ||||
|     if not current_invoice.invoice.paid and not current_invoice.invoice.void: | ||||
|         current_invoice.pay("Demo invoice payment", inv.value) | ||||
| 
 | ||||
|     return redirect("invoice", current_invoice.invoice.id) | ||||
|     form = forms.ManualPaymentForm( | ||||
|         request.POST or None, | ||||
|         prefix=FORM_PREFIX, | ||||
|     ) | ||||
| 
 | ||||
|     if request.POST and form.is_valid(): | ||||
|         form.instance.invoice = inv | ||||
|         form.save() | ||||
|         current_invoice.update_status() | ||||
|         form = forms.ManualPaymentForm(prefix=FORM_PREFIX) | ||||
| 
 | ||||
|     data = { | ||||
|         "invoice": inv, | ||||
|         "form": form, | ||||
|     } | ||||
| 
 | ||||
|     return render(request, "registrasion/manual_payment.html", data) | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer