From bb151dcb016fcf8a15c777cadcb094cdd7d31acc Mon Sep 17 00:00:00 2001 From: Bhumin Paladiya <139330832+bhumin18@users.noreply.github.com> Date: Thu, 3 Sep 2026 04:17:21 +0530 Subject: [PATCH] Fix order allocation variant validation logic and serializer read_only_fields (#12738) * Fix order allocation variant validation logic and serializer defects * Format order models and tests with ruff preview style * Fix TransferOrder destination field in allocation unit tests --- src/backend/InvenTree/order/models.py | 4 +- src/backend/InvenTree/order/serializers.py | 4 +- src/backend/InvenTree/order/test_api.py | 100 +++++++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index cdbc96daf2..544f997674 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -3124,7 +3124,7 @@ class SalesOrderAllocation(models.Model): try: if self.line.part != self.item.part: variants = self.line.part.get_descendants(include_self=True) - if self.line.part not in variants: + if self.item.part not in variants: errors['item'] = _( 'Cannot allocate stock item to a line with a different part' ) @@ -4127,7 +4127,7 @@ class TransferOrderAllocation(models.Model): try: if self.line.part != self.item.part: variants = self.line.part.get_descendants(include_self=True) - if self.line.part not in variants: + if self.item.part not in variants: errors['item'] = _( 'Cannot allocate stock item to a line with a different part' ) diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index d4fa0c81fd..4f40faa7f7 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -1584,7 +1584,7 @@ class SalesOrderAllocationSerializer( 'location_detail', 'shipment_detail', ] - read_only_fields = ['line', ''] + read_only_fields = ['line'] part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True) order = serializers.PrimaryKeyRelatedField( @@ -2891,7 +2891,7 @@ class TransferOrderAllocationSerializer( 'order_detail', 'location_detail', ] - read_only_fields = ['line', ''] + read_only_fields = ['line'] part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True) order = serializers.PrimaryKeyRelatedField( diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 421b64ebf2..a6103ce393 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -5853,3 +5853,103 @@ class OrderActionMissingPkTest(InvenTreeAPITestCase): ]: url = reverse(url_name, kwargs={'pk': 999999}) self.post(url, {}, expected_code=404) + + +class OrderAllocationValidationTest(InvenTreeAPITestCase): + """Unit tests for SalesOrderAllocation and TransferOrderAllocation validation.""" + + fixtures = ['company', 'users', 'location'] + + roles = [ + 'sales_order.add', + 'sales_order.change', + 'transfer_order.add', + 'transfer_order.change', + ] + + @classmethod + def setUpTestData(cls): + """Set up test data with base parts, variant parts, and unrelated parts.""" + super().setUpTestData() + + cls.customer = models.Company.objects.create( + name='Alloc Customer', is_customer=True, description='' + ) + cls.base_part = Part.objects.create( + name='Base Widget', salable=True, is_template=True, description='' + ) + cls.variant_part = Part.objects.create( + name='Variant Widget A', + salable=True, + variant_of=cls.base_part, + description='', + ) + cls.base_part.refresh_from_db() + cls.unrelated_part = Part.objects.create( + name='Unrelated Gadget', salable=True, description='' + ) + + cls.location = StockLocation.objects.first() + + def test_sales_order_allocation_validation(self): + """Test validation when allocating stock to a SalesOrder.""" + order = models.SalesOrder.objects.create( + customer=self.customer, reference='SO-ALLOC-TEST-1' + ) + line = models.SalesOrderLineItem.objects.create( + order=order, part=self.base_part, quantity=10 + ) + shipment = models.SalesOrderShipment.objects.create(order=order, reference='1') + + # Stock items + variant_stock = StockItem.objects.create( + part=self.variant_part, quantity=10, location=self.location + ) + unrelated_stock = StockItem.objects.create( + part=self.unrelated_part, quantity=10, location=self.location + ) + + # Allocating valid variant should succeed + alloc_variant = models.SalesOrderAllocation( + line=line, item=variant_stock, quantity=5, shipment=shipment + ) + alloc_variant.full_clean() + alloc_variant.save() + + # Allocating unrelated part should raise ValidationError + alloc_invalid = models.SalesOrderAllocation( + line=line, item=unrelated_stock, quantity=5, shipment=shipment + ) + with self.assertRaises(ValidationError): + alloc_invalid.full_clean() + + def test_transfer_order_allocation_validation(self): + """Test validation when allocating stock to a TransferOrder.""" + dest_loc = StockLocation.objects.create(name='Dest Location') + order = models.TransferOrder.objects.create( + destination=dest_loc, reference='TO-ALLOC-TEST-1' + ) + line = models.TransferOrderLineItem.objects.create( + order=order, part=self.base_part, quantity=10 + ) + + variant_stock = StockItem.objects.create( + part=self.variant_part, quantity=10, location=self.location + ) + unrelated_stock = StockItem.objects.create( + part=self.unrelated_part, quantity=10, location=self.location + ) + + # Allocating valid variant should succeed + alloc_variant = models.TransferOrderAllocation( + line=line, item=variant_stock, quantity=5 + ) + alloc_variant.full_clean() + alloc_variant.save() + + # Allocating unrelated part should raise ValidationError + alloc_invalid = models.TransferOrderAllocation( + line=line, item=unrelated_stock, quantity=5 + ) + with self.assertRaises(ValidationError): + alloc_invalid.full_clean()