Changeset - ae8f39381f4a
[Not reviewed]
0 6 0
Christopher Neugebauer - 8 years ago 2016-04-08 09:49:18
chrisjrn@gmail.com
Flake8 fixes
6 files changed with 4 insertions and 8 deletions:
0 comments (0 inline, 0 general)
registrasion/controllers/invoice.py
Show inline comments
 
from decimal import Decimal
 
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
 

	
 
from cart import CartController
 

	
 

	
 
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
 
    def for_cart(cls, cart):
 
        ''' Returns an invoice object for a given cart at its current revision.
 
        If such an invoice does not exist, the cart is validated, and if valid,
 
        an invoice is generated.'''
 

	
 
        try:
 
            invoice = rego.Invoice.objects.exclude(
 
                status=rego.Invoice.STATUS_VOID,
 
            ).get(
 
                cart=cart,
 
                cart_revision=cart.revision,
 
            )
 
        except ObjectDoesNotExist:
 
            cart_controller = CartController(cart)
 
            cart_controller.validate_cart()  # Raises ValidationError on fail.
 

	
 
            cls.void_all_invoices(cart)
 
            invoice = cls._generate(cart)
 

	
 
        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):
 
        try:
 
            condition = rego.DiscountForProduct.objects.get(
 
                discount=item.discount,
 
                product=item.product
 
            )
 
        except ObjectDoesNotExist:
 
            condition = rego.DiscountForCategory.objects.get(
 
                discount=item.discount,
 
                category=item.product.category
 
            )
 
        if condition.percentage is not None:
 
            value = item.product.price * (condition.percentage / 100)
 
        else:
 
            value = condition.price
 
        return value
 

	
 
    @classmethod
 
    @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,
 
            status=rego.Invoice.STATUS_UNPAID,
 
            value=Decimal(),
 
            issue_time=issued,
 
            due_time=due,
 
            recipient=recipient,
 
        )
 

	
 
        product_items = rego.ProductItem.objects.filter(cart=cart)
 

	
 
        if len(product_items) == 0:
 
            raise ValidationError("Your cart is empty.")
 

	
 
        product_items = product_items.order_by(
 
            "product__category__order", "product__order"
 
        )
 
        discount_items = rego.DiscountItem.objects.filter(cart=cart)
 
        invoice_value = Decimal()
 
        for item in product_items:
 
            product = item.product
 
            line_item = rego.LineItem.objects.create(
 
                invoice=invoice,
 
                description="%s - %s" % (product.category.name, product.name),
 
                quantity=item.quantity,
 
                price=product.price,
 
                product=product,
 
            )
 
            invoice_value += line_item.quantity * line_item.price
 

	
 
        for item in discount_items:
 
            line_item = rego.LineItem.objects.create(
 
                invoice=invoice,
 
                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
 

	
 
        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):
 
        ''' 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.status == rego.Invoice.STATUS_PAID:
 
            raise ValidationError("Paid invoices cannot be voided, "
 
                                  "only refunded.")
 
        self._mark_void()
 

	
 
    @transaction.atomic
 
    def refund(self, reference, amount):
 
        ''' 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.is_void:
 
            raise ValidationError("Void invoices cannot be refunded")
 

	
 
        # Adds a payment
 
        # TODO: replace by creating a credit note instead
 
        rego.ManualPayment.objects.create(
 
            invoice=self.invoice,
 
            reference=reference,
 
            amount=0 - amount,
 
        )
 

	
 
        self.update_status()
registrasion/tests/controller_helpers.py
Show inline comments
 
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):
 

	
 
    def set_quantity(self, product, quantity, batched=False):
 
        ''' Sets the _quantity_ of the given _product_ in the cart to the given
 
        _quantity_. '''
 

	
 
        self.set_quantities(((product, quantity),))
 

	
 
    def add_to_cart(self, product, quantity):
 
        ''' Adds _quantity_ of the given _product_ to the cart. Raises
 
        ValidationError if constraints are violated.'''
 

	
 
        try:
 
            product_item = rego.ProductItem.objects.get(
 
                cart=self.cart,
 
                product=product)
 
            old_quantity = product_item.quantity
 
        except ObjectDoesNotExist:
 
            old_quantity = 0
 
        self.set_quantity(product, old_quantity + quantity)
 

	
 
    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(
 
        rego.ManualPayment.objects.create(
 
            invoice=self.invoice,
 
            reference=reference,
 
            amount=amount,
 
        )
 

	
 
        self.update_status()
registrasion/tests/test_discount.py
Show inline comments
 
import pytz
 

	
 
from decimal import Decimal
 

	
 
from registrasion import models as rego
 
from registrasion.controllers import discount
 
from controller_helpers import TestingCartController
 
from controller_helpers import TestingInvoiceController
 

	
 
from test_cart import RegistrationCartTestCase
 

	
 
UTC = pytz.timezone('UTC')
 

	
 

	
 
class DiscountTestCase(RegistrationCartTestCase):
 

	
 
    @classmethod
 
    def add_discount_prod_1_includes_prod_2(
 
            cls,
 
            amount=Decimal(100),
 
            quantity=2,
 
            ):
 
        discount = rego.IncludedProductDiscount.objects.create(
 
            description="PROD_1 includes PROD_2 " + str(amount) + "%",
 
        )
 
        discount.save()
 
        discount.enabling_products.add(cls.PROD_1)
 
        discount.save()
 
        rego.DiscountForProduct.objects.create(
 
            discount=discount,
 
            product=cls.PROD_2,
 
            percentage=amount,
 
            quantity=quantity,
 
        ).save()
 
        return discount
 

	
 
    @classmethod
 
    def add_discount_prod_1_includes_cat_2(
 
            cls,
 
            amount=Decimal(100),
 
            quantity=2,
 
            ):
 
        discount = rego.IncludedProductDiscount.objects.create(
 
            description="PROD_1 includes CAT_2 " + str(amount) + "%",
 
        )
 
        discount.save()
 
        discount.enabling_products.add(cls.PROD_1)
 
        discount.save()
 
        rego.DiscountForCategory.objects.create(
 
            discount=discount,
 
            category=cls.CAT_2,
 
            percentage=amount,
 
            quantity=quantity,
 
        ).save()
 
        return discount
 

	
 
    @classmethod
 
    def add_discount_prod_1_includes_prod_3_and_prod_4(
 
            cls,
 
            amount=Decimal(100),
 
            quantity=2,
 
            ):
 
        discount = rego.IncludedProductDiscount.objects.create(
 
            description="PROD_1 includes PROD_3 and PROD_4 " +
 
                        str(amount) + "%",
 
        )
 
        discount.save()
 
        discount.enabling_products.add(cls.PROD_1)
 
        discount.save()
 
        rego.DiscountForProduct.objects.create(
 
            discount=discount,
 
            product=cls.PROD_3,
 
            percentage=amount,
 
            quantity=quantity,
 
        ).save()
 
        rego.DiscountForProduct.objects.create(
 
            discount=discount,
 
            product=cls.PROD_4,
 
            percentage=amount,
 
            quantity=quantity,
 
        ).save()
 
        return discount
 

	
 
    def test_discount_is_applied(self):
 
        self.add_discount_prod_1_includes_prod_2()
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 
        cart.add_to_cart(self.PROD_2, 1)
 

	
 
        # Discounts should be applied at this point...
 
        self.assertEqual(1, len(cart.cart.discountitem_set.all()))
 

	
 
    def test_discount_is_applied_for_category(self):
 
        self.add_discount_prod_1_includes_cat_2()
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 
        cart.add_to_cart(self.PROD_3, 1)
 

	
 
        # Discounts should be applied at this point...
 
        self.assertEqual(1, len(cart.cart.discountitem_set.all()))
 

	
 
    def test_discount_does_not_apply_if_not_met(self):
 
        self.add_discount_prod_1_includes_prod_2()
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_2, 1)
 

	
 
        # No discount should be applied as the condition is not met
 
        self.assertEqual(0, len(cart.cart.discountitem_set.all()))
 

	
 
    def test_discount_applied_out_of_order(self):
 
        self.add_discount_prod_1_includes_prod_2()
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_2, 1)
 
        cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # No discount should be applied as the condition is not met
 
        self.assertEqual(1, len(cart.cart.discountitem_set.all()))
 

	
 
    def test_discounts_collapse(self):
 
        self.add_discount_prod_1_includes_prod_2()
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 
        cart.add_to_cart(self.PROD_2, 1)
 
        cart.add_to_cart(self.PROD_2, 1)
 

	
 
        # Discounts should be applied and collapsed at this point...
 
        self.assertEqual(1, len(cart.cart.discountitem_set.all()))
 

	
 
    def test_discounts_respect_quantity(self):
 
        self.add_discount_prod_1_includes_prod_2()
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 
        cart.add_to_cart(self.PROD_2, 3)
 

	
 
        # There should be three items in the cart, but only two should
 
        # attract a discount.
 
        discount_items = list(cart.cart.discountitem_set.all())
 
        self.assertEqual(2, discount_items[0].quantity)
 

	
 
    def test_multiple_discounts_apply_in_order(self):
 
        discount_full = self.add_discount_prod_1_includes_prod_2()
 
        discount_half = self.add_discount_prod_1_includes_prod_2(Decimal(50))
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 
        cart.add_to_cart(self.PROD_2, 3)
 

	
 
        # There should be two discounts
 
        discount_items = list(cart.cart.discountitem_set.all())
 
        discount_items.sort(key=lambda item: item.quantity)
 
        self.assertEqual(2, len(discount_items))
 
        # The half discount should be applied only once
 
        self.assertEqual(1, discount_items[0].quantity)
 
        self.assertEqual(discount_half.pk, discount_items[0].discount.pk)
 
        # The full discount should be applied twice
 
        self.assertEqual(2, discount_items[1].quantity)
 
        self.assertEqual(discount_full.pk, discount_items[1].discount.pk)
 

	
 
    def test_discount_applies_across_carts(self):
 
        self.add_discount_prod_1_includes_prod_2()
 

	
 
        # Enable the discount during the first cart.
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 

	
 
        cart.next_cart()
 

	
 
        # Use the discount in the second cart
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_2, 1)
 

	
 
        # The discount should be applied.
 
        self.assertEqual(1, len(cart.cart.discountitem_set.all()))
 

	
 
        cart.next_cart()
 

	
 
        # The discount should respect the total quantity across all
 
        # of the user's carts.
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_2, 2)
 

	
 
        # Having one item in the second cart leaves one more item where
 
        # the discount is applicable. The discount should apply, but only for
 
        # quantity=1
 
        discount_items = list(cart.cart.discountitem_set.all())
 
        self.assertEqual(1, discount_items[0].quantity)
 

	
 
    def test_discount_applies_only_once_enabled(self):
 
        # Enable the discount during the first cart.
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 
        # This would exhaust discount if present
 
        cart.add_to_cart(self.PROD_2, 2)
 

	
registrasion/tests/test_invoice.py
Show inline comments
...
 
@@ -8,213 +8,211 @@ from registrasion import models as rego
 
from controller_helpers import TestingCartController
 
from controller_helpers import TestingInvoiceController
 

	
 
from test_cart import RegistrationCartTestCase
 

	
 
UTC = pytz.timezone('UTC')
 

	
 

	
 
class InvoiceTestCase(RegistrationCartTestCase):
 

	
 
    def test_create_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 = 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))
 
        # That invoice should have a value equal to cost of PROD_1
 
        self.assertEqual(self.PROD_1.price, invoice_1.invoice.value)
 

	
 
        # Adding item to cart should produce a new invoice
 
        current_cart.add_to_cart(self.PROD_2, 1)
 
        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.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)
 
        self.assertEqual(2, len(line_items))
 
        # Invoice should have a value equal to cost of PROD_1 and PROD_2
 
        self.assertEqual(
 
            self.PROD_1.price + self.PROD_2.price,
 
            invoice_2.invoice.value)
 

	
 
    def test_create_invoice_fails_if_cart_invalid(self):
 
        self.make_ceiling("Limit ceiling", limit=1)
 
        self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC))
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        self.add_timedelta(self.RESERVATION * 2)
 
        cart_2 = TestingCartController.for_user(self.USER_2)
 
        cart_2.add_to_cart(self.PROD_1, 1)
 

	
 
        # Now try to invoice the first user
 
        with self.assertRaises(ValidationError):
 
            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 = 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.is_paid)
 

	
 
        # Cart should not be active
 
        self.assertFalse(invoice.invoice.cart.active)
 

	
 
        # Asking for a cart should generate a new one
 
        new_cart = TestingCartController.for_user(self.USER_1)
 
        self.assertNotEqual(current_cart.cart, new_cart.cart)
 

	
 
    def test_invoice_includes_discounts(self):
 
        voucher = rego.Voucher.objects.create(
 
            recipient="Voucher recipient",
 
            code="VOUCHER",
 
            limit=1
 
        )
 
        discount = rego.VoucherDiscount.objects.create(
 
            description="VOUCHER RECIPIENT",
 
            voucher=voucher,
 
        )
 
        rego.DiscountForProduct.objects.create(
 
            discount=discount,
 
            product=self.PROD_1,
 
            percentage=Decimal(50),
 
            quantity=1
 
        )
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.apply_voucher(voucher.code)
 

	
 
        # Should be able to create an invoice after the product is added
 
        current_cart.add_to_cart(self.PROD_1, 1)
 
        invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
        # That invoice should have two line items
 
        line_items = rego.LineItem.objects.filter(invoice=invoice_1.invoice)
 
        self.assertEqual(2, len(line_items))
 
        # That invoice should have a value equal to 50% of the cost of PROD_1
 
        self.assertEqual(
 
            self.PROD_1.price * Decimal("0.5"),
 
            invoice_1.invoice.value)
 

	
 
    def test_zero_value_invoice_is_automatically_paid(self):
 
        voucher = rego.Voucher.objects.create(
 
            recipient="Voucher recipient",
 
            code="VOUCHER",
 
            limit=1
 
        )
 
        discount = rego.VoucherDiscount.objects.create(
 
            description="VOUCHER RECIPIENT",
 
            voucher=voucher,
 
        )
 
        rego.DiscountForProduct.objects.create(
 
            discount=discount,
 
            product=self.PROD_1,
 
            percentage=Decimal(100),
 
            quantity=1
 
        )
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.apply_voucher(voucher.code)
 

	
 
        # Should be able to create an invoice after the product is added
 
        current_cart.add_to_cart(self.PROD_1, 1)
 
        invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
        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 = TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
        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 = 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 = 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 = 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 = TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
        self.assertFalse(invoice_1.invoice.is_void)
 
        invoice_1.void()
 

	
 
        invoice_2 = TestingInvoiceController.for_cart(current_cart.cart)
 
        self.assertNotEqual(invoice_1.invoice, invoice_2.invoice)
 

	
 
    def test_cannot_pay_void_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 = TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
        invoice_1.void()
 

	
 
        with self.assertRaises(ValidationError):
 
            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 = TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
        invoice_1.pay("Reference", invoice_1.invoice.value)
 

	
 
        with self.assertRaises(ValidationError):
 
            invoice_1.void()
 

	
 
    def test_cannot_generate_blank_invoice(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        with self.assertRaises(ValidationError):
 
            invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
 
            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
registrasion/util.py
Show inline comments
 
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)
registrasion/views.py
Show inline comments
...
 
@@ -295,232 +295,232 @@ def product_category(request, category_id):
 
        )
 
        return redirect("dashboard")
 

	
 
    data = {
 
        "category": category,
 
        "discounts": discounts,
 
        "form": products_form,
 
        "voucher_form": voucher_form,
 
    }
 

	
 
    return render(request, "registrasion/product_category.html", data)
 

	
 

	
 
def handle_products(request, category, products, prefix):
 
    ''' Handles a products list form in the given request. Returns the
 
    form instance, the discounts applicable to this form, and whether the
 
    contents were handled. '''
 

	
 
    current_cart = CartController.for_user(request.user)
 

	
 
    ProductsForm = forms.ProductsForm(category, products)
 

	
 
    # Create initial data for each of products in category
 
    items = rego.ProductItem.objects.filter(
 
        product__in=products,
 
        cart=current_cart.cart,
 
    ).select_related("product")
 
    quantities = []
 
    seen = set()
 

	
 
    for item in items:
 
        quantities.append((item.product, item.quantity))
 
        seen.add(item.product)
 

	
 
    zeros = set(products) - seen
 
    for product in zeros:
 
        quantities.append((product, 0))
 

	
 
    products_form = ProductsForm(
 
        request.POST or None,
 
        product_quantities=quantities,
 
        prefix=prefix,
 
    )
 

	
 
    if request.method == "POST" and products_form.is_valid():
 
        if products_form.has_changed():
 
            set_quantities_from_products_form(products_form, current_cart)
 

	
 
        # If category is required, the user must have at least one
 
        # in an active+valid cart
 
        if category.required:
 
            carts = rego.Cart.objects.filter(user=request.user)
 
            items = rego.ProductItem.objects.filter(
 
                product__category=category,
 
                cart=carts,
 
            )
 
            if len(items) == 0:
 
                products_form.add_error(
 
                    None,
 
                    "You must have at least one item from this category",
 
                )
 
    handled = False if products_form.errors else True
 

	
 
    discounts = discount.available_discounts(request.user, [], products)
 

	
 
    return products_form, discounts, handled
 

	
 

	
 
def set_quantities_from_products_form(products_form, current_cart):
 

	
 
    quantities = list(products_form.product_quantities())
 

	
 
    pks = [i[0] for i in quantities]
 
    products = rego.Product.objects.filter(
 
        id__in=pks,
 
    ).select_related("category")
 

	
 
    product_quantities = [
 
        (products.get(pk=i[0]), i[1]) for i in quantities
 
    ]
 
    field_names = dict(
 
        (i[0][0], i[1][2]) for i in zip(product_quantities, quantities)
 
    )
 

	
 
    try:
 
        current_cart.set_quantities(product_quantities)
 
    except CartValidationError as ve:
 
        for ve_field in ve.error_list:
 
            product, message = ve_field.message
 
            if product in field_names:
 
                field = field_names[product]
 
            elif isinstance(product, rego.Product):
 
                continue
 
            else:
 
                field = None
 
            products_form.add_error(field, message)
 

	
 

	
 
def handle_voucher(request, prefix):
 
    ''' Handles a voucher form in the given request. Returns the voucher
 
    form instance, and whether the voucher code was handled. '''
 

	
 
    voucher_form = forms.VoucherForm(request.POST or None, prefix=prefix)
 
    current_cart = CartController.for_user(request.user)
 

	
 
    if (voucher_form.is_valid() and
 
            voucher_form.cleaned_data["voucher"].strip()):
 

	
 
        voucher = voucher_form.cleaned_data["voucher"]
 
        voucher = rego.Voucher.normalise_code(voucher)
 

	
 
        if len(current_cart.cart.vouchers.filter(code=voucher)) > 0:
 
            # This voucher has already been applied to this cart.
 
            # Do not apply code
 
            handled = False
 
        else:
 
            try:
 
                current_cart.apply_voucher(voucher)
 
            except Exception as e:
 
                voucher_form.add_error("voucher", e)
 
            handled = True
 
    else:
 
        handled = False
 

	
 
    return (voucher_form, handled)
 

	
 

	
 
@login_required
 
def checkout(request):
 
    ''' Runs checkout for the current cart of items, ideally generating an
 
    invoice. '''
 

	
 
    current_cart = CartController.for_user(request.user)
 

	
 
    if "fix_errors" in request.GET and request.GET["fix_errors"] == "true":
 
        current_cart.fix_simple_errors()
 

	
 
    try:
 
        current_invoice = InvoiceController.for_cart(current_cart.cart)
 
    except ValidationError as ve:
 
        return checkout_errors(request, ve)
 

	
 
    return redirect("invoice", current_invoice.invoice.id)
 

	
 

	
 
def checkout_errors(request, errors):
 

	
 
    error_list = []
 
    for error in errors.error_list:
 
        if isinstance(error, tuple):
 
            error = error[1]
 
        error_list.append(error)
 

	
 
    data = {
 
        "error_list": error_list,
 
    }
 

	
 
    return render(request, "registrasion/checkout_errors.html", data)
 

	
 

	
 
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)
 

	
 
    current_invoice = InvoiceController(inv)
 

	
 
    if not current_invoice.can_view(
 
            user=request.user,
 
            access_code=access_code,
 
        ):
 
            ):
 
        raise Http404()
 

	
 
    data = {
 
        "invoice": current_invoice.invoice,
 
    }
 

	
 
    return render(request, "registrasion/invoice.html", data)
 

	
 

	
 
@login_required
 
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 = get_object_or_404(rego.Invoice, pk=invoice_id)
 
    current_invoice = InvoiceController(inv)
 

	
 
    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)
0 comments (0 inline, 0 general)