commit
						1cf6645061
					
				
					 2 changed files with 103 additions and 37 deletions
				
			
		|  | @ -44,7 +44,7 @@ class InvoiceController(ForId, object): | ||||||
|             cart_controller.validate_cart()  # Raises ValidationError on fail. |             cart_controller.validate_cart()  # Raises ValidationError on fail. | ||||||
| 
 | 
 | ||||||
|             cls.update_old_invoices(cart) |             cls.update_old_invoices(cart) | ||||||
|             invoice = cls._generate(cart) |             invoice = cls._generate_from_cart(cart) | ||||||
| 
 | 
 | ||||||
|         return cls(invoice) |         return cls(invoice) | ||||||
| 
 | 
 | ||||||
|  | @ -74,45 +74,57 @@ class InvoiceController(ForId, object): | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     @transaction.atomic |     @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. ''' |         ''' Generates an invoice for the given cart. ''' | ||||||
| 
 | 
 | ||||||
|         cart.refresh_from_db() |         cart.refresh_from_db() | ||||||
| 
 | 
 | ||||||
|         issued = timezone.now() |         # Generate the line items from the cart. | ||||||
|         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, |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|         product_items = commerce.ProductItem.objects.filter(cart=cart) |         product_items = commerce.ProductItem.objects.filter(cart=cart) | ||||||
|         product_items = product_items.select_related( |         product_items = product_items.select_related( | ||||||
|             "product", |             "product", | ||||||
|             "product__category", |             "product__category", | ||||||
|         ) |         ) | ||||||
| 
 |  | ||||||
|         if len(product_items) == 0: |  | ||||||
|             raise ValidationError("Your cart is empty.") |  | ||||||
| 
 |  | ||||||
|         product_items = product_items.order_by( |         product_items = product_items.order_by( | ||||||
|             "product__category__order", "product__order" |             "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 = commerce.DiscountItem.objects.filter(cart=cart) | ||||||
|         discount_items = discount_items.select_related( |         discount_items = discount_items.select_related( | ||||||
|             "discount", |             "discount", | ||||||
|  | @ -120,8 +132,6 @@ class InvoiceController(ForId, object): | ||||||
|             "product__category", |             "product__category", | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         line_items = [] |  | ||||||
| 
 |  | ||||||
|         def format_product(product): |         def format_product(product): | ||||||
|             return "%s - %s" % (product.category.name, product.name) |             return "%s - %s" % (product.category.name, product.name) | ||||||
| 
 | 
 | ||||||
|  | @ -129,35 +139,65 @@ class InvoiceController(ForId, object): | ||||||
|             description = discount.description |             description = discount.description | ||||||
|             return "%s (%s)" % (description, format_product(product)) |             return "%s (%s)" % (description, format_product(product)) | ||||||
| 
 | 
 | ||||||
|         invoice_value = Decimal() |         line_items = [] | ||||||
|  | 
 | ||||||
|         for item in product_items: |         for item in product_items: | ||||||
|             product = item.product |             product = item.product | ||||||
|             line_item = commerce.LineItem( |             line_item = commerce.LineItem( | ||||||
|                 invoice=invoice, |  | ||||||
|                 description=format_product(product), |                 description=format_product(product), | ||||||
|                 quantity=item.quantity, |                 quantity=item.quantity, | ||||||
|                 price=product.price, |                 price=product.price, | ||||||
|                 product=product, |                 product=product, | ||||||
|             ) |             ) | ||||||
|             line_items.append(line_item) |             line_items.append(line_item) | ||||||
|             invoice_value += line_item.quantity * line_item.price |  | ||||||
|         for item in discount_items: |         for item in discount_items: | ||||||
|             line_item = commerce.LineItem( |             line_item = commerce.LineItem( | ||||||
|                 invoice=invoice, |  | ||||||
|                 description=format_discount(item.discount, item.product), |                 description=format_discount(item.discount, item.product), | ||||||
|                 quantity=item.quantity, |                 quantity=item.quantity, | ||||||
|                 price=cls.resolve_discount_value(item) * -1, |                 price=cls.resolve_discount_value(item) * -1, | ||||||
|                 product=item.product, |                 product=item.product, | ||||||
|             ) |             ) | ||||||
|             line_items.append(line_item) |             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) |         commerce.LineItem.objects.bulk_create(line_items) | ||||||
| 
 | 
 | ||||||
|         invoice.value = invoice_value |  | ||||||
| 
 |  | ||||||
|         invoice.save() |  | ||||||
| 
 |  | ||||||
|         cls.email_on_invoice_creation(invoice) |         cls.email_on_invoice_creation(invoice) | ||||||
| 
 | 
 | ||||||
|         return invoice |         return invoice | ||||||
|  |  | ||||||
|  | @ -580,6 +580,32 @@ class InvoiceTestCase(RegistrationCartTestCase): | ||||||
|             cn2.credit_note.value, |             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): |     def test_sends_email_on_invoice_creation(self): | ||||||
|         invoice = self._invoice_containing_prod_1(1) |         invoice = self._invoice_containing_prod_1(1) | ||||||
|         self.assertEquals(1, len(self.emails)) |         self.assertEquals(1, len(self.emails)) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer