diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 16ac01aa34..563b0b3696 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -1602,7 +1602,9 @@ class SalesOrderAllocation(models.Model): try: if self.line.part != self.item.part: - errors['item'] = _('Cannot allocate stock item to a line with a different part') + variants = self.line.part.get_descendants(include_self=True) + if self.line.part not in variants: + errors['item'] = _('Cannot allocate stock item to a line with a different part') except PartModels.Part.DoesNotExist: errors['line'] = _('Cannot allocate stock to a line without a part') diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 99ee7bc894..9f6e30a88e 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -1716,7 +1716,7 @@ class SalesOrderAllocateTest(OrderTest): self.assertIn('Shipment is not associated with this order', str(response.data['shipment'])) def test_allocate(self): - """Test the the allocation endpoint acts as expected, when provided with valid data!""" + """Test that the allocation endpoint acts as expected, when provided with valid data!""" # First, check that there are no line items allocated against this SalesOrder self.assertEqual(self.order.stock_allocations.count(), 0) @@ -1745,6 +1745,43 @@ class SalesOrderAllocateTest(OrderTest): for line in self.order.lines.all(): self.assertEqual(line.allocations.count(), 1) + def test_allocate_variant(self): + """Test that the allocation endpoint acts as expected, when provided with variant""" + # First, check that there are no line items allocated against this SalesOrder + self.assertEqual(self.order.stock_allocations.count(), 0) + + data = { + "items": [], + "shipment": self.shipment.pk, + } + + def check_template(line_item): + return line_item.part.is_template + + for line in filter(check_template, self.order.lines.all()): + + stock_item = None + + # Allocate a matching variant + parts = Part.objects.filter(salable=True).filter(variant_of=line.part.pk) + for part in parts: + stock_item = part.stock_items.last() + break + + # Fully-allocate each line + data['items'].append({ + "line_item": line.pk, + "stock_item": stock_item.pk, + "quantity": 5 + }) + + self.post(self.url, data, expected_code=201) + + # At least one item should be allocated, and all should be variants + self.assertGreater(self.order.stock_allocations.count(), 0) + for allocation in self.order.stock_allocations.all(): + self.assertNotEquals(allocation.item.part.pk, allocation.line.part.pk) + def test_shipment_complete(self): """Test that we can complete a shipment via the API.""" url = reverse('api-so-shipment-ship', kwargs={'pk': self.shipment.pk}) diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py index 7d787b91ff..9ac410be05 100644 --- a/InvenTree/order/test_sales_order.py +++ b/InvenTree/order/test_sales_order.py @@ -33,11 +33,23 @@ class SalesOrderTest(TestCase): cls.customer = Company.objects.create(name="ABC Co", description="My customer", is_customer=True) # Create a Part to ship - cls.part = Part.objects.create(name='Spanner', salable=True, description='A spanner that I sell') + cls.part = Part.objects.create( + name='Spanner', + salable=True, + description='A spanner that I sell', + is_template=True, + ) + cls.variant = Part.objects.create( + name='Blue Spanner', + salable=True, + description='A blue spanner that I sell', + variant_of=cls.part, + ) # Create some stock! cls.Sa = StockItem.objects.create(part=cls.part, quantity=100) cls.Sb = StockItem.objects.create(part=cls.part, quantity=200) + cls.Sc = StockItem.objects.create(part=cls.variant, quantity=100) # Create a SalesOrder to ship against cls.order = SalesOrder.objects.create( @@ -145,6 +157,16 @@ class SalesOrderTest(TestCase): self.assertTrue(self.line.is_fully_allocated()) self.assertEqual(self.line.allocated_quantity(), 50) + def test_allocate_variant(self): + """Allocate a variant of the designated item""" + SalesOrderAllocation.objects.create( + line=self.line, + shipment=self.shipment, + item=StockItem.objects.get(pk=self.Sc.pk), + quantity=50 + ) + self.assertEqual(self.line.allocated_quantity(), 50) + def test_order_cancel(self): """Allocate line items then cancel the order""" self.allocate_stock(True) @@ -166,8 +188,8 @@ class SalesOrderTest(TestCase): def test_complete_order(self): """Allocate line items, then ship the order""" # Assert some stuff before we run the test - # Initially there are two stock items - self.assertEqual(StockItem.objects.count(), 2) + # Initially there are three stock items + self.assertEqual(StockItem.objects.count(), 3) # Take 25 units from each StockItem self.allocate_stock(True) @@ -194,15 +216,17 @@ class SalesOrderTest(TestCase): self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED) self.assertIsNotNone(self.order.shipment_date) - # There should now be 4 stock items - self.assertEqual(StockItem.objects.count(), 4) + # There should now be 5 stock items + self.assertEqual(StockItem.objects.count(), 5) sa = StockItem.objects.get(pk=self.Sa.pk) sb = StockItem.objects.get(pk=self.Sb.pk) + sc = StockItem.objects.get(pk=self.Sc.pk) - # 25 units subtracted from each of the original items + # 25 units subtracted from each of the original non-variant items self.assertEqual(sa.quantity, 75) self.assertEqual(sb.quantity, 175) + self.assertEqual(sc.quantity, 100) # And 2 items created which are associated with the order outputs = StockItem.objects.filter(sales_order=self.order) diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml index a25a919520..3ff48e5ca3 100644 --- a/InvenTree/part/fixtures/part.yaml +++ b/InvenTree/part/fixtures/part.yaml @@ -156,6 +156,7 @@ variant_of: 10000 IPN: "R.CH" trackable: true + salable: true category: 7 tree_id: 1 level: 0