diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index 2c69faed..a2adca4e 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,45 +74,57 @@ class InvoiceController(ForId, object): @classmethod @transaction.atomic - def _generate(cls, cart): + 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): ''' Generates an invoice for the given cart. ''' 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( "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", @@ -120,8 +132,6 @@ class InvoiceController(ForId, object): "product__category", ) - line_items = [] - def format_product(product): return "%s - %s" % (product.category.name, product.name) @@ -129,35 +139,65 @@ class InvoiceController(ForId, object): description = discount.description return "%s (%s)" % (description, format_product(product)) - invoice_value = Decimal() + line_items = [] + 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 + + 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) - invoice.value = invoice_value - - invoice.save() - cls.email_on_invoice_creation(invoice) return invoice 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))