mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Fix annotations for returning serialized StockItems as lists (#9969)
* Fix annotations/pagination on StockApi itemSerialize and BuildApi outputCreate * Add (to schema) field to specify serial numbers on create for stock item * Return list on StockItem creation * Update api version * Update test to expect list return when creating stock items * Add note about breaking changes to api version * Add handling for stockitem list return on creation * Update api version --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com> Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
		| @@ -1,11 +1,14 @@ | ||||
| """InvenTree API version information.""" | ||||
|  | ||||
| # InvenTree API version | ||||
| INVENTREE_API_VERSION = 382 | ||||
| INVENTREE_API_VERSION = 383 | ||||
|  | ||||
| """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" | ||||
|  | ||||
| INVENTREE_API_TEXT = """ | ||||
| v383 -> 2025-08-08 : https://github.com/inventree/InvenTree/pull/9969 | ||||
|     - Correctly apply changes listed in v358 | ||||
|     - Breaking: StockCreate now always returns a list of StockItem | ||||
|  | ||||
| v382 -> 2025-08-07 : https://github.com/inventree/InvenTree/pull/10146 | ||||
|     - Adds ability to "bulk create" test results via the API | ||||
|   | ||||
| @@ -117,6 +117,16 @@ class ExtendedAutoSchema(AutoSchema): | ||||
|                         f'{parameter["description"]} Searched fields: {", ".join(search_fields)}.' | ||||
|                     ) | ||||
|  | ||||
|         # Change return to array type, simply annotating this return type attempts to paginate, which doesn't work for | ||||
|         # a create method and removing the pagination also affects the list method | ||||
|         if self.method == 'POST' and type(self.view).__name__ == 'StockList': | ||||
|             schema = operation['responses']['201']['content']['application/json'][ | ||||
|                 'schema' | ||||
|             ] | ||||
|             schema['type'] = 'array' | ||||
|             schema['items'] = {'$ref': schema['$ref']} | ||||
|             del schema['$ref'] | ||||
|  | ||||
|         return operation | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -631,14 +631,15 @@ class BuildOrderContextMixin: | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| @extend_schema(responses={201: stock.serializers.StockItemSerializer(many=True)}) | ||||
| class BuildOutputCreate(BuildOrderContextMixin, CreateAPI): | ||||
|     """API endpoint for creating new build output(s).""" | ||||
|  | ||||
|     queryset = Build.objects.none() | ||||
|  | ||||
|     serializer_class = build.serializers.BuildOutputCreateSerializer | ||||
|     pagination_class = None | ||||
|  | ||||
|     @extend_schema(responses={201: stock.serializers.StockItemSerializer(many=True)}) | ||||
|     def create(self, request, *args, **kwargs): | ||||
|         """Override the create method to handle the creation of build outputs.""" | ||||
|         serializer = self.get_serializer(data=request.data) | ||||
|   | ||||
| @@ -120,12 +120,13 @@ class StockItemContextMixin: | ||||
|         return context | ||||
|  | ||||
|  | ||||
| @extend_schema(responses={201: StockSerializers.StockItemSerializer(many=True)}) | ||||
| class StockItemSerialize(StockItemContextMixin, CreateAPI): | ||||
|     """API endpoint for serializing a stock item.""" | ||||
|  | ||||
|     serializer_class = StockSerializers.SerializeStockItemSerializer | ||||
|     pagination_class = None | ||||
|  | ||||
|     @extend_schema(responses={201: StockSerializers.StockItemSerializer(many=True)}) | ||||
|     def create(self, request, *args, **kwargs): | ||||
|         """Serialize the provided StockItem.""" | ||||
|         serializer = self.get_serializer(data=request.data) | ||||
| @@ -1182,7 +1183,7 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView): | ||||
|  | ||||
|                 item.save(user=user) | ||||
|  | ||||
|                 response_data = serializer.data | ||||
|                 response_data = [serializer.data] | ||||
|  | ||||
|         return Response( | ||||
|             response_data, | ||||
|   | ||||
| @@ -371,6 +371,7 @@ class StockItemSerializer( | ||||
|             'purchase_price', | ||||
|             'purchase_price_currency', | ||||
|             'use_pack_size', | ||||
|             'serial_numbers', | ||||
|             'tests', | ||||
|             # Annotated fields | ||||
|             'allocated', | ||||
| @@ -402,7 +403,10 @@ class StockItemSerializer( | ||||
|         """ | ||||
|         Fields used when creating a stock item | ||||
|         """ | ||||
|         extra_kwargs = {'use_pack_size': {'write_only': True}} | ||||
|         extra_kwargs = { | ||||
|             'use_pack_size': {'write_only': True}, | ||||
|             'serial_numbers': {'write_only': True}, | ||||
|         } | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         """Add detail fields.""" | ||||
| @@ -467,7 +471,14 @@ class StockItemSerializer( | ||||
|         help_text=_( | ||||
|             'Use pack size when adding: the quantity defined is the number of packs' | ||||
|         ), | ||||
|         label=('Use pack size'), | ||||
|         label=_('Use pack size'), | ||||
|     ) | ||||
|  | ||||
|     serial_numbers = serializers.CharField( | ||||
|         write_only=True, | ||||
|         required=False, | ||||
|         allow_null=True, | ||||
|         help_text=_('Enter serial numbers for new items'), | ||||
|     ) | ||||
|  | ||||
|     def validate_part(self, part): | ||||
|   | ||||
| @@ -1133,9 +1133,9 @@ class CustomStockItemStatusTest(StockAPITestCase): | ||||
|             }, | ||||
|             expected_code=201, | ||||
|         ) | ||||
|         self.assertEqual(response.data['status'], self.status.logical_key) | ||||
|         self.assertEqual(response.data['status_custom_key'], self.status.key) | ||||
|         pk = response.data['pk'] | ||||
|         self.assertEqual(response.data[0]['status'], self.status.logical_key) | ||||
|         self.assertEqual(response.data[0]['status_custom_key'], self.status.key) | ||||
|         pk = response.data[0]['pk'] | ||||
|  | ||||
|         # Update the stock item with another custom status code via the API | ||||
|         response = self.patch( | ||||
| @@ -1167,8 +1167,8 @@ class CustomStockItemStatusTest(StockAPITestCase): | ||||
|             }, | ||||
|             expected_code=201, | ||||
|         ) | ||||
|         self.assertEqual(response.data['status'], self.status.logical_key) | ||||
|         self.assertEqual(response.data['status_custom_key'], self.status.logical_key) | ||||
|         self.assertEqual(response.data[0]['status'], self.status.logical_key) | ||||
|         self.assertEqual(response.data[0]['status_custom_key'], self.status.logical_key) | ||||
|  | ||||
|         # Test case with wrong key | ||||
|         response = self.patch( | ||||
| @@ -1216,7 +1216,7 @@ class StockItemTest(StockAPITestCase): | ||||
|             self.list_url, data={'part': 4, 'quantity': 10}, expected_code=201 | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(response.data['location'], 2) | ||||
|         self.assertEqual(response.data[0]['location'], 2) | ||||
|  | ||||
|         # What if we explicitly set the location to a different value? | ||||
|  | ||||
| @@ -1225,7 +1225,7 @@ class StockItemTest(StockAPITestCase): | ||||
|             data={'part': 4, 'quantity': 20, 'location': 1}, | ||||
|             expected_code=201, | ||||
|         ) | ||||
|         self.assertEqual(response.data['location'], 1) | ||||
|         self.assertEqual(response.data[0]['location'], 1) | ||||
|  | ||||
|         # And finally, what if we set the location explicitly to None? | ||||
|  | ||||
| @@ -1235,7 +1235,7 @@ class StockItemTest(StockAPITestCase): | ||||
|             expected_code=201, | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(response.data['location'], None) | ||||
|         self.assertEqual(response.data[0]['location'], None) | ||||
|  | ||||
|     def test_stock_item_create(self): | ||||
|         """Test creation of a StockItem via the API.""" | ||||
| @@ -1306,7 +1306,7 @@ class StockItemTest(StockAPITestCase): | ||||
|         # Reload part, count stock again | ||||
|         part_4 = part.models.Part.objects.get(pk=4) | ||||
|         self.assertEqual(part_4.available_stock, current_count + 3) | ||||
|         stock_4 = StockItem.objects.get(pk=response.data['pk']) | ||||
|         stock_4 = StockItem.objects.get(pk=response.data[0]['pk']) | ||||
|         self.assertEqual(stock_4.purchase_price, Money('123.450000', 'USD')) | ||||
|  | ||||
|         # POST with valid supplier part, no pack size defined | ||||
| @@ -1330,7 +1330,7 @@ class StockItemTest(StockAPITestCase): | ||||
|         # Reload part, count stock again | ||||
|         part_4 = part.models.Part.objects.get(pk=4) | ||||
|         self.assertEqual(part_4.available_stock, current_count + 12) | ||||
|         stock_4 = StockItem.objects.get(pk=response.data['pk']) | ||||
|         stock_4 = StockItem.objects.get(pk=response.data[0]['pk']) | ||||
|         self.assertEqual(stock_4.purchase_price, Money('123.450000', 'USD')) | ||||
|  | ||||
|         # POST with valid supplier part, WITH pack size defined - but ignore | ||||
| @@ -1352,7 +1352,7 @@ class StockItemTest(StockAPITestCase): | ||||
|         # Reload part, count stock again | ||||
|         part_4 = part.models.Part.objects.get(pk=4) | ||||
|         self.assertEqual(part_4.available_stock, current_count + 3) | ||||
|         stock_4 = StockItem.objects.get(pk=response.data['pk']) | ||||
|         stock_4 = StockItem.objects.get(pk=response.data[0]['pk']) | ||||
|         self.assertEqual(stock_4.purchase_price, Money('123.450000', 'USD')) | ||||
|  | ||||
|         # POST with valid supplier part, WITH pack size defined and used | ||||
| @@ -1374,7 +1374,7 @@ class StockItemTest(StockAPITestCase): | ||||
|         # Reload part, count stock again | ||||
|         part_4 = part.models.Part.objects.get(pk=4) | ||||
|         self.assertEqual(part_4.available_stock, current_count + 3 * 100) | ||||
|         stock_4 = StockItem.objects.get(pk=response.data['pk']) | ||||
|         stock_4 = StockItem.objects.get(pk=response.data[0]['pk']) | ||||
|         self.assertEqual(stock_4.purchase_price, Money('1.234500', 'USD')) | ||||
|  | ||||
|     def test_creation_with_serials(self): | ||||
| @@ -1450,15 +1450,15 @@ class StockItemTest(StockAPITestCase): | ||||
|  | ||||
|         response = self.post(self.list_url, data, expected_code=201) | ||||
|  | ||||
|         self.assertIsNone(response.data['expiry_date']) | ||||
|         self.assertIsNone(response.data[0]['expiry_date']) | ||||
|  | ||||
|         # Second test - create a new StockItem with an explicit expiry date | ||||
|         data['expiry_date'] = '2022-12-12' | ||||
|  | ||||
|         response = self.post(self.list_url, data, expected_code=201) | ||||
|  | ||||
|         self.assertIsNotNone(response.data['expiry_date']) | ||||
|         self.assertEqual(response.data['expiry_date'], '2022-12-12') | ||||
|         self.assertIsNotNone(response.data[0]['expiry_date']) | ||||
|         self.assertEqual(response.data[0]['expiry_date'], '2022-12-12') | ||||
|  | ||||
|         # Third test - create a new StockItem for a Part which has a default expiry time | ||||
|         data = {'part': 25, 'quantity': 10} | ||||
| @@ -1468,13 +1468,13 @@ class StockItemTest(StockAPITestCase): | ||||
|         # Expected expiry date is 10 days in the future | ||||
|         expiry = datetime.now().date() + timedelta(10) | ||||
|  | ||||
|         self.assertEqual(response.data['expiry_date'], expiry.isoformat()) | ||||
|         self.assertEqual(response.data[0]['expiry_date'], expiry.isoformat()) | ||||
|  | ||||
|         # Test result when sending a blank value | ||||
|         data['expiry_date'] = None | ||||
|  | ||||
|         response = self.post(self.list_url, data, expected_code=201) | ||||
|         self.assertEqual(response.data['expiry_date'], expiry.isoformat()) | ||||
|         self.assertEqual(response.data[0]['expiry_date'], expiry.isoformat()) | ||||
|  | ||||
|     def test_purchase_price(self): | ||||
|         """Test that we can correctly read and adjust purchase price information via the API.""" | ||||
| @@ -1843,7 +1843,7 @@ class StockItemDeletionTest(StockAPITestCase): | ||||
|                 expected_code=201, | ||||
|             ) | ||||
|  | ||||
|             pk = response.data['pk'] | ||||
|             pk = response.data[0]['pk'] | ||||
|  | ||||
|             self.assertEqual(StockItem.objects.count(), n + 1) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user