Files @ 8ad265a65a08
Branch filter:

Location: symposion_app/registrasion/controllers/invoice.py

Christopher Neugebauer
Fixes tests now that $0 invoices pay themselves
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 registrasion import models as rego

from cart import CartController


class InvoiceController(object):

    def __init__(self, invoice):
        self.invoice = invoice
        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.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)

            invoice = cls._generate(cart)

        return InvoiceController(invoice)

    @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. '''
        invoice = rego.Invoice.objects.create(
            user=cart.user,
            cart=cart,
            cart_revision=cart.revision,
            value=Decimal()
        )

        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,
            )
            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,
            )
            invoice_value += line_item.quantity * line_item.price

        invoice.value = invoice_value

        if invoice.value == 0:
            invoice.paid = True

        invoice.save()

        return invoice

    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()

    def void(self):
        ''' Voids the invoice if it is valid to do so. '''
        if self.invoice.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()

    @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.
        '''

        if self.invoice.void:
            raise ValidationError("Void invoices cannot be refunded")

        ''' Adds a payment '''
        payment = rego.Payment.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()