Changeset - fd9980efc578
[Not reviewed]
0 2 0
Christopher Neugebauer - 8 years ago 2016-09-15 01:41:50
chrisjrn@gmail.com
Makes sure we only apply unclaimed credit notes when auto-applying credit notes.
2 files changed with 15 insertions and 1 deletions:
0 comments (0 inline, 0 general)
registrasion/controllers/invoice.py
Show inline comments
...
 
@@ -124,193 +124,195 @@ class InvoiceController(ForId, object):
 

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

	
 
        discount_items = commerce.DiscountItem.objects.filter(cart=cart)
 
        discount_items = discount_items.select_related(
 
            "discount",
 
            "product",
 
            "product__category",
 
        )
 

	
 
        def format_product(product):
 
            return "%s - %s" % (product.category.name, product.name)
 

	
 
        def format_discount(discount, product):
 
            description = discount.description
 
            return "%s (%s)" % (description, format_product(product))
 

	
 
        line_items = []
 

	
 
        for item in product_items:
 
            product = item.product
 
            line_item = commerce.LineItem(
 
                description=format_product(product),
 
                quantity=item.quantity,
 
                price=product.price,
 
                product=product,
 
            )
 
            line_items.append(line_item)
 
        for item in discount_items:
 
            line_item = commerce.LineItem(
 
                description=format_discount(item.discount, item.product),
 
                quantity=item.quantity,
 
                price=cls.resolve_discount_value(item) * -1,
 
                product=item.product,
 
            )
 
            line_items.append(line_item)
 

	
 
        # Generate the invoice
 

	
 
        min_due_time = cart.reservation_duration + cart.time_last_updated
 

	
 
        return cls._generate(cart.user, cart, min_due_time, line_items)
 

	
 
    @classmethod
 
    @transaction.atomic
 
    def _generate(cls, user, cart, min_due_time, line_items):
 

	
 
        # Never generate a due time that is before the issue time
 
        issued = timezone.now()
 
        due = max(issued, min_due_time)
 

	
 
        # Get the invoice recipient
 
        profile = people.AttendeeProfileBase.objects.get_subclass(
 
            id=user.attendee.attendeeprofilebase.id,
 
        )
 
        recipient = profile.invoice_recipient()
 

	
 
        invoice_value = sum(item.quantity * item.price for item in line_items)
 

	
 
        invoice = commerce.Invoice.objects.create(
 
            user=user,
 
            cart=cart,
 
            cart_revision=cart.revision if cart else None,
 
            status=commerce.Invoice.STATUS_UNPAID,
 
            value=invoice_value,
 
            issue_time=issued,
 
            due_time=due,
 
            recipient=recipient,
 
        )
 

	
 
        # Associate the line items with the invoice
 
        for line_item in line_items:
 
            line_item.invoice = invoice
 

	
 
        commerce.LineItem.objects.bulk_create(line_items)
 

	
 
        cls._apply_credit_notes(invoice)
 
        cls.email_on_invoice_creation(invoice)
 

	
 
        return invoice
 

	
 
    @classmethod
 
    def _apply_credit_notes(cls, invoice):
 
        ''' Applies the user's credit notes to the given invoice on creation.
 
        '''
 

	
 
        # We only automatically apply credit notes if this is the *only*
 
        # unpaid invoice for this user.
 
        invoices = commerce.Invoice.objects.filter(
 
            user=invoice.user,
 
            status=commerce.Invoice.STATUS_UNPAID,
 
        )
 
        if invoices.count() > 1:
 
            return
 

	
 
        notes = commerce.CreditNote.objects.filter(invoice__user=invoice.user)
 
        notes = commerce.CreditNote.unclaimed().filter(
 
            invoice__user=invoice.user
 
        )
 
        for note in notes:
 
            try:
 
                CreditNoteController(note).apply_to_invoice(invoice)
 
            except ValidationError:
 
                # ValidationError will get raised once we're overpaying.
 
                break
 

	
 
        invoice.refresh_from_db()
 

	
 
    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 = commerce.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 = commerce.PaymentBase.objects.filter(
 
            invoice=self.invoice,
 
        ).count()
 
        remainder = self.invoice.value - total_paid
 

	
 
        if old_status == commerce.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 == commerce.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 == commerce.Invoice.STATUS_REFUNDED:
 
            # Should not ever change from here
 
            pass
 
        elif old_status == commerce.Invoice.STATUS_VOID:
 
            # Should not ever change from here
 
            pass
 

	
 
        # Generate credit notes from residual payments
 
        residual = 0
 
        if self.invoice.is_paid:
 
            if remainder < 0:
 
                residual = 0 - remainder
 
        elif self.invoice.is_void or self.invoice.is_refunded:
 
            residual = total_paid
 

	
registrasion/tests/test_credit_note.py
Show inline comments
...
 
@@ -335,96 +335,108 @@ class CreditNoteTestCase(TestHelperMixin, RegistrationCartTestCase):
 
        self.assertEquals(
 
            cn.credit_note.value,
 
            cn2.credit_note.value,
 
        )
 

	
 
    def test_creating_invoice_automatically_applies_credit_note(self):
 
        ''' Single credit note is automatically applied to new invoices. '''
 

	
 
        invoice = self._invoice_containing_prod_1(1)
 
        invoice.pay("boop", invoice.invoice.value)
 
        invoice.refund()
 

	
 
        # Generate a new invoice to the same value as first invoice
 
        # Should be paid, because we're applying credit notes automatically
 
        invoice2 = self._invoice_containing_prod_1(1)
 
        self.assertTrue(invoice2.invoice.is_paid)
 

	
 
    def _generate_multiple_credit_notes(self):
 
        invoice1 = self._manual_invoice(11)
 
        invoice2 = self._manual_invoice(11)
 
        invoice1.pay("Pay", invoice1.invoice.value)
 
        invoice1.refund()
 
        invoice2.pay("Pay", invoice2.invoice.value)
 
        invoice2.refund()
 
        return invoice1.invoice.value + invoice2.invoice.value
 

	
 
    def test_mutiple_credit_notes_are_applied_when_generating_invoice_1(self):
 
        ''' Tests (1) that multiple credit notes are applied to new invoice.
 

	
 
        Sum of credit note values will be *LESS* than the new invoice.
 
        '''
 

	
 
        notes_value = self._generate_multiple_credit_notes()
 
        invoice = self._manual_invoice(notes_value + 1)
 

	
 
        self.assertEqual(notes_value, invoice.total_payments())
 
        self.assertTrue(invoice.invoice.is_unpaid)
 

	
 
        user_unclaimed = commerce.CreditNote.unclaimed()
 
        user_unclaimed = user_unclaimed.filter(invoice__user=self.USER_1)
 
        self.assertEqual(0, user_unclaimed.count())
 

	
 
    def test_mutiple_credit_notes_are_applied_when_generating_invoice_2(self):
 
        ''' Tests (2) that multiple credit notes are applied to new invoice.
 

	
 
        Sum of credit note values will be *GREATER* than the new invoice.
 
        '''
 

	
 
        notes_value = self._generate_multiple_credit_notes()
 
        invoice = self._manual_invoice(notes_value - 1)
 

	
 

	
 
        self.assertEqual(notes_value - 1, invoice.total_payments())
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        user_unclaimed = commerce.CreditNote.unclaimed().filter(
 
            invoice__user=self.USER_1
 
        )
 
        self.assertEqual(1, user_unclaimed.count())
 

	
 
        excess = self._credit_note_for_invoice(invoice.invoice)
 
        self.assertEqual(excess.credit_note.value, 1)
 

	
 
    def test_credit_notes_are_left_over_if_not_all_are_needed(self):
 
        ''' Tests that excess credit notes are untouched if they're not needed
 
        '''
 

	
 
        notes_value = self._generate_multiple_credit_notes()
 
        notes_old = commerce.CreditNote.unclaimed().filter(
 
            invoice__user=self.USER_1
 
        )
 

	
 
        # Create a manual invoice whose value is smaller than any of the
 
        # credit notes we created
 
        invoice = self._manual_invoice(1)
 
        notes_new = commerce.CreditNote.unclaimed().filter(
 
            invoice__user=self.USER_1
 
        )
 

	
 
        # Item is True if the note was't consumed when generating invoice.
 
        note_was_unused = [(i in notes_old) for i in notes_new]
 
        self.assertIn(True, note_was_unused)
 

	
 
    def test_credit_notes_are_not_applied_if_user_has_multiple_invoices(self):
 

	
 
        # Have an invoice pending with no credit notes; no payment will be made
 
        invoice1 = self._invoice_containing_prod_1(1)
 
        # Create some credit notes.
 
        self._generate_multiple_credit_notes()
 

	
 
        invoice = self._manual_invoice(2)
 

	
 
        # Because there's already an invoice open for this user
 
        # The credit notes are not automatically applied.
 
        self.assertEqual(0, invoice.total_payments())
 
        self.assertTrue(invoice.invoice.is_unpaid)
 

	
 
    def test_credit_notes_are_applied_even_if_some_notes_are_claimed(self):
 

	
 
        for i in xrange(10):
 
            # Generate credit note
 
            invoice1 = self._manual_invoice(1)
 
            invoice1.pay("Pay", invoice1.invoice.value)
 
            invoice1.refund()
 

	
 
            # Generate invoice that should be automatically paid
 
            invoice2 = self._manual_invoice(1)
 
            self.assertTrue(invoice2.invoice.is_paid)
0 comments (0 inline, 0 general)