From 613667aa30e93894428f6824f5d75f7209129ced Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 14 Sep 2016 19:44:36 +1000 Subject: [PATCH 1/4] Re-arranges invoice generation code. - Reduces number of db queries - Localises the code that interrogates the cart and the code that generates the invoice itself. --- registrasion/controllers/invoice.py | 61 +++++++++++++++-------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index 2c69faed..b88abb09 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -79,26 +79,7 @@ class InvoiceController(ForId, object): cart.refresh_from_db() - 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 = people.AttendeeProfileBase.objects.get_subclass( - id=cart.user.attendee.attendeeprofilebase.id, - ) - recipient = profile.invoice_recipient() - invoice = commerce.Invoice.objects.create( - user=cart.user, - cart=cart, - cart_revision=cart.revision, - status=commerce.Invoice.STATUS_UNPAID, - value=Decimal(), - issue_time=issued, - due_time=due, - recipient=recipient, - ) + # Generate the line items from the cart. product_items = commerce.ProductItem.objects.filter(cart=cart) product_items = product_items.select_related( @@ -129,35 +110,57 @@ class InvoiceController(ForId, object): description = discount.description return "%s (%s)" % (description, format_product(product)) - invoice_value = Decimal() for item in product_items: product = item.product line_item = commerce.LineItem( - invoice=invoice, description=format_product(product), quantity=item.quantity, price=product.price, product=product, ) line_items.append(line_item) - invoice_value += line_item.quantity * line_item.price for item in discount_items: line_item = commerce.LineItem( - invoice=invoice, 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) - invoice_value += line_item.quantity * line_item.price + + # Generate the invoice + + user = cart.user + reservation_limit = cart.reservation_duration + cart.time_last_updated + # Never generate a due time that is before the issue time + issued = timezone.now() + due = max(issued, reservation_limit) + + # 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, + 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) - invoice.value = invoice_value - - invoice.save() - cls.email_on_invoice_creation(invoice) return invoice From a9bc6475707c6f6aaea7f512b9140cc67abc54d2 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 15 Sep 2016 08:38:33 +1000 Subject: [PATCH 2/4] Replaces _generate with _generate_from_cart and _generate --- registrasion/controllers/invoice.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index b88abb09..118b49cb 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -44,7 +44,7 @@ class InvoiceController(ForId, object): cart_controller.validate_cart() # Raises ValidationError on fail. cls.update_old_invoices(cart) - invoice = cls._generate(cart) + invoice = cls._generate_from_cart(cart) return cls(invoice) @@ -74,7 +74,7 @@ class InvoiceController(ForId, object): @classmethod @transaction.atomic - def _generate(cls, cart): + def _generate_from_cart(cls, cart): ''' Generates an invoice for the given cart. ''' cart.refresh_from_db() @@ -86,14 +86,13 @@ class InvoiceController(ForId, object): "product", "product__category", ) - - if len(product_items) == 0: - raise ValidationError("Your cart is empty.") - product_items = product_items.order_by( "product__category__order", "product__order" ) + 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", @@ -101,8 +100,6 @@ class InvoiceController(ForId, object): "product__category", ) - line_items = [] - def format_product(product): return "%s - %s" % (product.category.name, product.name) @@ -110,6 +107,8 @@ class InvoiceController(ForId, object): description = discount.description return "%s (%s)" % (description, format_product(product)) + line_items = [] + for item in product_items: product = item.product line_item = commerce.LineItem( @@ -131,10 +130,17 @@ class InvoiceController(ForId, object): # Generate the invoice user = cart.user - reservation_limit = cart.reservation_duration + cart.time_last_updated + 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, reservation_limit) + due = max(issued, min_due_time) # Get the invoice recipient profile = people.AttendeeProfileBase.objects.get_subclass( From 2e5a8e3668908ddefbce0d4dcfb9b701098e248f Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 15 Sep 2016 08:50:56 +1000 Subject: [PATCH 3/4] First pass at allowing manual invoices. --- registrasion/controllers/invoice.py | 33 ++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index 118b49cb..3d4df7fc 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -72,6 +72,38 @@ class InvoiceController(ForId, object): value = condition.price return value + @classmethod + @transaction.atomic + def manual_invoice(cls, user, due_delta, description_price_pairs): + ''' Generates an invoice for arbitrary items, not held in a user's + cart. + + Arguments: + user (User): The user the invoice is being generated for. + due_delta (datetime.timedelta): The length until the invoice is + due. + description_price_pairs ([(str, long or Decimal), ...]): A list of + pairs. Each pair consists of the description for each line item + and the price for that line item. The price will be cast to + Decimal. + + Returns: + an Invoice. + ''' + + line_items = [] + for description, price in description_price_pairs: + line_item = commerce.LineItem( + description=description, + quantity=1, + price=Decimal(price), + product=None, + ) + line_items.append(line_item) + + min_due_time = timezone.now() + due_delta + return cls._generate(user, None, min_due_time, line_items) + @classmethod @transaction.atomic def _generate_from_cart(cls, cart): @@ -129,7 +161,6 @@ class InvoiceController(ForId, object): # Generate the invoice - user = cart.user min_due_time = cart.reservation_duration + cart.time_last_updated return cls._generate(cart.user, cart, min_due_time, line_items) From 6469bcd8e78a749a4069ccefdf254038204c543a Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 15 Sep 2016 09:08:29 +1000 Subject: [PATCH 4/4] Adds test for manual invoicing --- registrasion/controllers/invoice.py | 2 +- registrasion/tests/test_invoice.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index 3d4df7fc..a2adca4e 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -184,7 +184,7 @@ class InvoiceController(ForId, object): invoice = commerce.Invoice.objects.create( user=user, cart=cart, - cart_revision=cart.revision, + cart_revision=cart.revision if cart else None, status=commerce.Invoice.STATUS_UNPAID, value=invoice_value, issue_time=issued, diff --git a/registrasion/tests/test_invoice.py b/registrasion/tests/test_invoice.py index 6d36d082..4f73bf3c 100644 --- a/registrasion/tests/test_invoice.py +++ b/registrasion/tests/test_invoice.py @@ -580,6 +580,32 @@ class InvoiceTestCase(RegistrationCartTestCase): cn2.credit_note.value, ) + def test_can_generate_manual_invoice(self): + + description_price_pairs = [ + ("Item 1", 15), + ("Item 2", 30), + ] + + due_delta = datetime.timedelta(hours=24) + + _invoice = TestingInvoiceController.manual_invoice( + self.USER_1, due_delta, description_price_pairs + ) + inv = TestingInvoiceController(_invoice) + + self.assertEquals( + inv.invoice.value, + sum(i[1] for i in description_price_pairs) + ) + + self.assertEquals( + len(inv.invoice.lineitem_set.all()), + len(description_price_pairs) + ) + + inv.pay("Demo payment", inv.invoice.value) + def test_sends_email_on_invoice_creation(self): invoice = self._invoice_containing_prod_1(1) self.assertEquals(1, len(self.emails))