mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Return Order - Improvements (#8590)
* Increase query limit * Add "quantity" field to ReturnOrderLineItem model * Add 'quantity' to serializer * Optionally split stock when returning from customer * Update the line item when splitting * PUI updates * Bump API version * Add unit test
This commit is contained in:
		| @@ -1,13 +1,16 @@ | ||||
| """InvenTree API version information.""" | ||||
|  | ||||
| # InvenTree API version | ||||
| INVENTREE_API_VERSION = 289 | ||||
| INVENTREE_API_VERSION = 290 | ||||
|  | ||||
| """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" | ||||
|  | ||||
|  | ||||
| INVENTREE_API_TEXT = """ | ||||
|  | ||||
| v290 - 2024-11-29 : https://github.com/inventree/InvenTree/pull/8590 | ||||
|     - Adds "quantity" field to ReturnOrderLineItem model and API | ||||
|  | ||||
| v289 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8570 | ||||
|     - Enable status change when transferring stock items | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,19 @@ | ||||
| # Generated by Django 4.2.16 on 2024-11-29 00:37 | ||||
|  | ||||
| import django.core.validators | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('order', '0103_alter_salesorderallocation_shipment'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='returnorderlineitem', | ||||
|             name='quantity', | ||||
|             field=models.DecimalField(decimal_places=5, default=1, help_text='Quantity to return', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -2391,6 +2391,14 @@ class ReturnOrder(TotalPriceMixin, Order): | ||||
|  | ||||
|         stock_item = line.item | ||||
|  | ||||
|         if not stock_item.serialized and line.quantity < stock_item.quantity: | ||||
|             # Split the stock item if we are returning less than the full quantity | ||||
|             stock_item = stock_item.splitStock(line.quantity, user=user) | ||||
|  | ||||
|             # Update the line item to point to the *new* stock item | ||||
|             line.item = stock_item | ||||
|             line.save() | ||||
|  | ||||
|         status = kwargs.get('status') | ||||
|  | ||||
|         if status is None: | ||||
| @@ -2423,7 +2431,7 @@ class ReturnOrder(TotalPriceMixin, Order): | ||||
|         line.received_date = InvenTree.helpers.current_date() | ||||
|         line.save() | ||||
|  | ||||
|         trigger_event(ReturnOrderEvents.RECEIVED, id=self.pk) | ||||
|         trigger_event(ReturnOrderEvents.RECEIVED, id=self.pk, line_item_id=line.pk) | ||||
|  | ||||
|         # Notify responsible users | ||||
|         notify_responsible( | ||||
| @@ -2452,9 +2460,22 @@ class ReturnOrderLineItem(OrderLineItem): | ||||
|         """Perform extra validation steps for the ReturnOrderLineItem model.""" | ||||
|         super().clean() | ||||
|  | ||||
|         if self.item and not self.item.serialized: | ||||
|         if not self.item: | ||||
|             raise ValidationError({'item': _('Stock item must be specified')}) | ||||
|  | ||||
|         if self.quantity > self.item.quantity: | ||||
|             raise ValidationError({ | ||||
|                 'item': _('Only serialized items can be assigned to a Return Order') | ||||
|                 'quantity': _('Return quantity exceeds stock quantity') | ||||
|             }) | ||||
|  | ||||
|         if self.quantity <= 0: | ||||
|             raise ValidationError({ | ||||
|                 'quantity': _('Return quantity must be greater than zero') | ||||
|             }) | ||||
|  | ||||
|         if self.item.serialized and self.quantity != 1: | ||||
|             raise ValidationError({ | ||||
|                 'quantity': _('Invalid quantity for serialized stock item') | ||||
|             }) | ||||
|  | ||||
|     order = models.ForeignKey( | ||||
| @@ -2473,6 +2494,15 @@ class ReturnOrderLineItem(OrderLineItem): | ||||
|         help_text=_('Select item to return from customer'), | ||||
|     ) | ||||
|  | ||||
|     quantity = models.DecimalField( | ||||
|         verbose_name=('Quantity'), | ||||
|         help_text=('Quantity to return'), | ||||
|         max_digits=15, | ||||
|         decimal_places=5, | ||||
|         validators=[MinValueValidator(0)], | ||||
|         default=1, | ||||
|     ) | ||||
|  | ||||
|     received_date = models.DateField( | ||||
|         null=True, | ||||
|         blank=True, | ||||
|   | ||||
| @@ -2040,6 +2040,7 @@ class ReturnOrderLineItemSerializer( | ||||
|             'order_detail', | ||||
|             'item', | ||||
|             'item_detail', | ||||
|             'quantity', | ||||
|             'received_date', | ||||
|             'outcome', | ||||
|             'part_detail', | ||||
| @@ -2070,9 +2071,15 @@ class ReturnOrderLineItemSerializer( | ||||
|             self.fields.pop('part_detail', None) | ||||
|  | ||||
|     order_detail = ReturnOrderSerializer(source='order', many=False, read_only=True) | ||||
|  | ||||
|     quantity = serializers.FloatField( | ||||
|         label=_('Quantity'), help_text=_('Quantity to return') | ||||
|     ) | ||||
|  | ||||
|     item_detail = stock.serializers.StockItemSerializer( | ||||
|         source='item', many=False, read_only=True | ||||
|     ) | ||||
|  | ||||
|     part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True) | ||||
|  | ||||
|     price = InvenTreeMoneySerializer(allow_null=True) | ||||
|   | ||||
| @@ -2395,6 +2395,71 @@ class ReturnOrderTests(InvenTreeAPITestCase): | ||||
|         self.assertEqual(deltas['location'], 1) | ||||
|         self.assertEqual(deltas['returnorder'], rma.pk) | ||||
|  | ||||
|     def test_receive_untracked(self): | ||||
|         """Test that we can receive untracked items against a ReturnOrder. | ||||
|  | ||||
|         Ref: https://github.com/inventree/InvenTree/pull/8590 | ||||
|         """ | ||||
|         self.assignRole('return_order.add') | ||||
|         company = Company.objects.get(pk=4) | ||||
|  | ||||
|         # Create a new ReturnOrder | ||||
|         rma = models.ReturnOrder.objects.create( | ||||
|             customer=company, description='A return order' | ||||
|         ) | ||||
|  | ||||
|         rma.issue_order() | ||||
|  | ||||
|         # Create some new line items | ||||
|         part = Part.objects.get(pk=25) | ||||
|  | ||||
|         n_items = part.stock_entries().count() | ||||
|  | ||||
|         for idx in range(2): | ||||
|             stock_item = StockItem.objects.create( | ||||
|                 part=part, customer=company, quantity=10 | ||||
|             ) | ||||
|  | ||||
|             models.ReturnOrderLineItem.objects.create( | ||||
|                 order=rma, item=stock_item, quantity=(idx + 1) * 5 | ||||
|             ) | ||||
|  | ||||
|         self.assertEqual(part.stock_entries().count(), n_items + 2) | ||||
|  | ||||
|         line_items = rma.lines.all() | ||||
|  | ||||
|         # Receive items against the order | ||||
|         url = reverse('api-return-order-receive', kwargs={'pk': rma.pk}) | ||||
|  | ||||
|         LOCATION_ID = 1 | ||||
|  | ||||
|         self.post( | ||||
|             url, | ||||
|             { | ||||
|                 'items': [ | ||||
|                     {'item': line.pk, 'status': StockStatus.DAMAGED.value} | ||||
|                     for line in line_items | ||||
|                 ], | ||||
|                 'location': LOCATION_ID, | ||||
|             }, | ||||
|             expected_code=201, | ||||
|         ) | ||||
|  | ||||
|         # Due to the quantities received, we should have created 1 new stock item | ||||
|         self.assertEqual(part.stock_entries().count(), n_items + 3) | ||||
|  | ||||
|         rma.refresh_from_db() | ||||
|  | ||||
|         for line in rma.lines.all(): | ||||
|             self.assertTrue(line.received) | ||||
|             self.assertIsNotNone(line.received_date) | ||||
|  | ||||
|             # Check that the associated StockItem has been updated correctly | ||||
|             self.assertEqual(line.item.status, StockStatus.DAMAGED) | ||||
|             self.assertIsNone(line.item.customer) | ||||
|             self.assertIsNone(line.item.sales_order) | ||||
|             self.assertEqual(line.item.location.pk, LOCATION_ID) | ||||
|  | ||||
|     def test_ro_calendar(self): | ||||
|         """Test the calendar export endpoint.""" | ||||
|         # Full test is in test_po_calendar. Since these use the same backend, test only | ||||
|   | ||||
		Reference in New Issue
	
	Block a user