Changeset - 6a5e4ff92db8
[Not reviewed]
Merge
0 4 0
Christopher Neugebauer - 8 years ago 2016-10-13 16:37:26
chrisjrn@gmail.com
Merge branch 'chrisjrn/20161013'
4 files changed with 54 insertions and 3 deletions:
0 comments (0 inline, 0 general)
registrasion/controllers/invoice.py
Show inline comments
...
 
@@ -312,98 +312,108 @@ class InvoiceController(ForId, object):
 
        if residual != 0:
 
            CreditNoteController.generate_from_invoice(self.invoice, residual)
 

	
 
        self.email_on_invoice_change(
 
            self.invoice,
 
            old_status,
 
            self.invoice.status,
 
        )
 

	
 
    def _mark_paid(self):
 
        ''' Marks the invoice as paid, and updates the attached cart if
 
        necessary. '''
 
        cart = self.invoice.cart
 
        if cart:
 
            cart.status = commerce.Cart.STATUS_PAID
 
            cart.save()
 
        self.invoice.status = commerce.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.status = commerce.Cart.STATUS_RELEASED
 
            cart.save()
 
        self.invoice.status = commerce.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 = commerce.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. '''
 

	
 
        self._refresh()
 

	
 
        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():
 
        ''' Voids this invoice if the attached cart is no longer valid because
 
        the cart revision has changed, or the reservations have expired. '''
 

	
 
        is_valid = self._invoice_matches_cart()
 
        cart = self.invoice.cart
 
        if self.invoice.is_unpaid and is_valid and cart:
 
            try:
 
                CartController(cart).validate_cart()
 
            except ValidationError:
 
                is_valid = False
 

	
 
        if not is_valid:
 
            if self.invoice.total_payments() > 0:
 
                # Free up the payments made to this invoice
 
                self.refund()
 
            else:
 
                self.void()
 

	
 
    def void(self):
 
        ''' Voids the invoice if it is valid to do so. '''
 
        if self.invoice.total_payments() > 0:
 
            raise ValidationError("Invoices with payments must be refunded.")
 
        elif self.invoice.is_refunded:
 
            raise ValidationError("Refunded invoices may not be voided.")
 
        self._mark_void()
 

	
 
    @transaction.atomic
 
    def refund(self):
 
        ''' Refunds the invoice by generating a CreditNote for the value of
 
        all of the payments against the cart.
 

	
 
        The invoice is marked as refunded, and the underlying cart is marked
 
        as released.
 

	
 
        '''
 

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

	
 
        # Raises a credit note fot the value of the invoice.
 
        amount = self.invoice.total_payments()
 

	
 
        if amount == 0:
 
            self.void()
 
            return
 

	
 
        CreditNoteController.generate_from_invoice(self.invoice, amount)
 
        self.update_status()
 

	
 
    @classmethod
 
    def email(cls, invoice, kind):
 
        ''' Sends out an e-mail notifying the user about something to do
 
        with that invoice. '''
 

	
 
        context = {
 
            "invoice": invoice,
 
        }
 

	
 
        send_email([invoice.user.email], kind, context=context)
 

	
registrasion/reporting/views.py
Show inline comments
...
 
@@ -337,96 +337,111 @@ def paid_invoices_by_date(request, form):
 
    invoice_max_time = payments.values("invoice").annotate(
 
        max_time=Max("time")
 
    )
 

	
 
    # Zero-value invoices will have no payments, so they're paid at issue time
 
    zero_value_invoices = invoices.filter(value=0)
 

	
 
    times = itertools.chain(
 
        (line["max_time"] for line in invoice_max_time),
 
        (invoice.issue_time for invoice in zero_value_invoices),
 
    )
 

	
 
    by_date = collections.defaultdict(int)
 
    for time in times:
 
        date = datetime.datetime(
 
            year=time.year, month=time.month, day=time.day
 
        )
 
        by_date[date] += 1
 

	
 
    data = [(date, count) for date, count in sorted(by_date.items())]
 
    data = [(date.strftime("%Y-%m-%d"), count) for date, count in data]
 

	
 
    return ListReport(
 
        "Paid Invoices By Date",
 
        ["date", "count"],
 
        data,
 
    )
 

	
 
@report_view("Credit notes")
 
def credit_notes(request, form):
 
    ''' Shows all of the credit notes in the system. '''
 

	
 
    notes = commerce.CreditNote.objects.all().select_related(
 
        "creditnoterefund",
 
        "creditnoteapplication",
 
        "invoice",
 
        "invoice__user__attendee__attendeeprofilebase",
 
    )
 

	
 
    return QuerysetReport(
 
        "Credit Notes",
 
        ["id", "invoice__user__attendee__attendeeprofilebase__invoice_recipient", "status", "value"],  # NOQA
 
        notes,
 
        headings=["id", "Owner", "Status", "Value"],
 
        link_view=views.credit_note,
 
    )
 

	
 

	
 
@report_view("Invoices")
 
def invoices(request,form):
 
    ''' Shows all of the invoices in the system. '''
 

	
 
    invoices = commerce.Invoice.objects.all().order_by("status")
 

	
 
    return QuerysetReport(
 
        "Invoices",
 
        ["id", "recipient", "value", "get_status_display"],
 
        invoices,
 
        headings=["id", "Recipient", "Value", "Status"],
 
        link_view=views.invoice,
 
    )
 

	
 

	
 
class AttendeeListReport(ListReport):
 

	
 
    def get_link(self, argument):
 
        return reverse(self._link_view) + "?user=%d" % int(argument)
 

	
 

	
 
@report_view("Attendee", form_type=forms.UserIdForm)
 
def attendee(request, form, user_id=None):
 
    ''' Returns a list of all manifested attendees if no attendee is specified,
 
    else displays the attendee manifest. '''
 

	
 
    if user_id is None and not form.has_changed():
 
        return attendee_list(request)
 

	
 
    if form.cleaned_data["user"] is not None:
 
        user_id = form.cleaned_data["user"]
 

	
 
    attendee = people.Attendee.objects.get(user__id=user_id)
 
    name = attendee.attendeeprofilebase.attendee_name()
 

	
 
    reports = []
 

	
 
    profile_data = []
 
    try:
 
        profile = people.AttendeeProfileBase.objects.get_subclass(
 
            attendee=attendee
 
        )
 
        fields = profile._meta.get_fields()
 
    except people.AttendeeProfileBase.DoesNotExist:
 
        fields = []
 

	
 
    exclude = set(["attendeeprofilebase_ptr", "id"])
 
    for field in fields:
 
        if field.name in exclude:
 
            # Not actually important
 
            continue
 
        if not hasattr(field, "verbose_name"):
 
            continue  # Not a publicly visible field
 
        value = getattr(profile, field.name)
 

	
 
        if isinstance(field, models.ManyToManyField):
 
            value = ", ".join(str(i) for i in value.all())
 

	
 
        profile_data.append((field.verbose_name, value))
 

	
 
    cart = CartController.for_user(attendee.user)
 
    reservation = cart.cart.reservation_duration + cart.cart.time_last_updated
 
    profile_data.append(("Current cart reserved until", reservation))
registrasion/tests/test_invoice.py
Show inline comments
...
 
@@ -123,118 +123,143 @@ class InvoiceTestCase(TestHelperMixin, RegistrationCartTestCase):
 
            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 = commerce.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 = inventory.Voucher.objects.create(
 
            recipient="Voucher recipient",
 
            code="VOUCHER",
 
            limit=1
 
        )
 
        discount = conditions.VoucherDiscount.objects.create(
 
            description="VOUCHER RECIPIENT",
 
            voucher=voucher,
 
        )
 
        conditions.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):
 
    def test_invoice_voids_self_if_cart_changes(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_invoice_voids_self_if_cart_becomes_invalid(self):
 
        ''' Invoices should be void if cart becomes invalid over time '''
 

	
 
        self.make_ceiling("Limit ceiling", limit=1)
 
        self.set_time(datetime.datetime(
 
            year=2015, month=1, day=1, hour=0, minute=0, tzinfo=UTC,
 
        ))
 

	
 
        cart1 = TestingCartController.for_user(self.USER_1)
 
        cart2 = TestingCartController.for_user(self.USER_2)
 

	
 
        # Create a valid invoice for USER_1
 
        cart1.add_to_cart(self.PROD_1, 1)
 
        inv1 = TestingInvoiceController.for_cart(cart1.cart)
 

	
 
        # Expire the reservations, and have USER_2 take up PROD_1's ceiling
 
        # generate an invoice
 
        self.add_timedelta(self.RESERVATION * 2)
 
        cart2.add_to_cart(self.PROD_2, 1)
 
        inv2 = TestingInvoiceController.for_cart(cart2.cart)
 

	
 
        # Re-get inv1's invoice; it should void itself on loading.
 
        inv1 = TestingInvoiceController(inv1.invoice)
 
        self.assertTrue(inv1.invoice.is_void)
 

	
 
    def test_voiding_invoice_creates_new_invoice(self):
 
        invoice_1 = self._invoice_containing_prod_1(1)
 

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

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

	
 
    def test_cannot_pay_void_invoice(self):
 
        invoice_1 = self._invoice_containing_prod_1(1)
 

	
 
        invoice_1.void()
 

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

	
 
    def test_cannot_void_paid_invoice(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

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

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

	
 
    def test_cannot_void_partially_paid_invoice(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        invoice.pay("Reference", invoice.invoice.value - 1)
 
        self.assertTrue(invoice.invoice.is_unpaid)
 

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

	
 
    def test_cannot_generate_blank_invoice(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        with self.assertRaises(ValidationError):
 
            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):
registrasion/urls.py
Show inline comments
...
 
@@ -4,67 +4,68 @@ from django.conf.urls import include
 
from django.conf.urls import url
 

	
 
from .views import (
 
    amend_registration,
 
    checkout,
 
    credit_note,
 
    edit_profile,
 
    extend_reservation,
 
    guided_registration,
 
    invoice,
 
    invoice_access,
 
    manual_payment,
 
    product_category,
 
    refund,
 
    review,
 
)
 

	
 

	
 
public = [
 
    url(r"^amend/([0-9]+)$", amend_registration, name="amend_registration"),
 
    url(r"^category/([0-9]+)$", product_category, name="product_category"),
 
    url(r"^checkout$", checkout, name="checkout"),
 
    url(r"^checkout/([0-9]+)$", checkout, name="checkout"),
 
    url(r"^credit_note/([0-9]+)$", credit_note, name="credit_note"),
 
    url(r"^extend/([0-9]+)$", extend_reservation, name="extend_reservation"),
 
    url(r"^invoice/([0-9]+)$", invoice, name="invoice"),
 
    url(r"^invoice/([0-9]+)/([A-Z0-9]+)$", invoice, name="invoice"),
 
    url(r"^invoice/([0-9]+)/manual_payment$",
 
        manual_payment, name="manual_payment"),
 
    url(r"^invoice/([0-9]+)/refund$",
 
        refund, name="refund"),
 
    url(r"^invoice_access/([A-Z0-9]+)$", invoice_access,
 
        name="invoice_access"),
 
    url(r"^profile$", edit_profile, name="attendee_edit"),
 
    url(r"^register$", guided_registration, name="guided_registration"),
 
    url(r"^review$", review, name="review"),
 
    url(r"^register/([0-9]+)$", guided_registration,
 
        name="guided_registration"),
 
]
 

	
 

	
 
reports = [
 
    url(r"^$", rv.reports_list, name="reports_list"),
 
    url(r"^attendee/?$", rv.attendee, name="attendee"),
 
    url(r"^attendee_data/?$", rv.attendee_data, name="attendee_data"),
 
    url(r"^attendee/([0-9]*)$", rv.attendee, name="attendee"),
 
    url(r"^credit_notes/?$", rv.credit_notes, name="credit_notes"),
 
    url(r"^discount_status/?$", rv.discount_status, name="discount_status"),
 
    url(r"^invoices/?$", rv.invoices, name="invoices"),
 
    url(
 
        r"^paid_invoices_by_date/?$",
 
        rv.paid_invoices_by_date,
 
        name="paid_invoices_by_date"
 
    ),
 
    url(r"^product_status/?$", rv.product_status, name="product_status"),
 
    url(r"^reconciliation/?$", rv.reconciliation, name="reconciliation"),
 
    url(
 
        r"^speaker_registrations/?$",
 
        rv.speaker_registrations,
 
        name="speaker_registrations",
 
    ),
 
]
 

	
 

	
 
urlpatterns = [
 
    url(r"^reports/", include(reports)),
 
    url(r"^", include(public))  # This one must go last.
 
]
0 comments (0 inline, 0 general)