From f6664b2477f4c8a2fa882760ddaeaf34439f2754 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Apr 2022 22:50:13 +1000 Subject: [PATCH 01/15] Add annotated fields to BomItem API: - total-stock / allocated_to_build_orders / allocated_to_sales_orders --- InvenTree/part/api.py | 3 +- InvenTree/part/serializers.py | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index f7bb81520d..911e5453a5 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -1601,9 +1601,10 @@ class BomList(generics.ListCreateAPIView): def get_queryset(self, *args, **kwargs): - queryset = BomItem.objects.all() + queryset = super().get_queryset(*args, **kwargs) queryset = self.get_serializer_class().setup_eager_loading(queryset) + queryset = self.get_serializer_class().annotate_queryset(queryset) return queryset diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index c352c59eab..09bb4799a0 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -577,6 +577,11 @@ class BomItemSerializer(InvenTreeModelSerializer): purchase_price_range = serializers.SerializerMethodField() + # Annotated fields + total_stock = serializers.FloatField(read_only=True) + allocated_to_sales_orders = serializers.FloatField(read_only=True) + allocated_to_build_orders = serializers.FloatField(read_only=True) + def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. # This saves a bunch of database requests @@ -613,6 +618,59 @@ class BomItemSerializer(InvenTreeModelSerializer): queryset = queryset.prefetch_related('sub_part__supplier_parts__pricebreaks') return queryset + @staticmethod + def annotate_queryset(queryset): + """ + Annotate the BomItem queryset with extra information: + + Annotations: + available_stock: The amount of stock available for the sub_part Part object + """ + + """ + Construct an "available stock" quantity: + + available_stock = total_stock - build_order_allocations - sales_order_allocations + """ + + # Calculate "total stock" for the referenced sub_part + # Calculate the "build_order_allocations" for the sub_part + queryset = queryset.annotate( + total_stock=Coalesce( + SubquerySum('sub_part__stock_items__quantity', filter=StockItem.IN_STOCK_FILTER), + Decimal(0), + output_field=models.DecimalField(), + ), + allocated_to_sales_orders=Coalesce( + SubquerySum( + 'sub_part__stock_items__sales_order_allocations__quantity', + filter=Q( + line__order__status__in=SalesOrderStatus.OPEN, + shipment__shipment_date=None, + ) + ), + Decimal(0), + output_field=models.DecimalField(), + ), + allocated_to_build_orders=Coalesce( + SubquerySum( + 'sub_part__stock_items__allocations__quantity', + filter=Q( + build__status__in=BuildStatus.ACTIVE_CODES, + ), + ), + Decimal(0), + output_field=models.DecimalField(), + ) + # build_order_allocations=Coalesce( + # SubquerySum('sub_part__stock_items__allocations__quantity'), + # ) + ) + + # queryset = querty + + return queryset + def get_purchase_price_range(self, obj): """ Return purchase price range """ @@ -682,6 +740,11 @@ class BomItemSerializer(InvenTreeModelSerializer): 'substitutes', 'price_range', 'validated', + + 'total_stock', + 'allocated_to_sales_orders', + 'allocated_to_build_orders', + ] From c6ba104ae8e219a97e596c1e821e8f0015caedf4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Apr 2022 23:07:44 +1000 Subject: [PATCH 02/15] Condense into single "available_stock" field --- InvenTree/part/serializers.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 09bb4799a0..5d62d5414b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -578,9 +578,7 @@ class BomItemSerializer(InvenTreeModelSerializer): purchase_price_range = serializers.SerializerMethodField() # Annotated fields - total_stock = serializers.FloatField(read_only=True) - allocated_to_sales_orders = serializers.FloatField(read_only=True) - allocated_to_build_orders = serializers.FloatField(read_only=True) + available_stock = serializers.FloatField(read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. @@ -614,7 +612,11 @@ class BomItemSerializer(InvenTreeModelSerializer): queryset = queryset.prefetch_related('sub_part') queryset = queryset.prefetch_related('sub_part__category') - queryset = queryset.prefetch_related('sub_part__stock_items') + queryset = queryset.prefetch_related( + 'sub_part__stock_items', + 'sub_part__stock_items__allocations', + 'sub_part__stock_items__sales_order_allocations', + ) queryset = queryset.prefetch_related('sub_part__supplier_parts__pricebreaks') return queryset @@ -635,7 +637,7 @@ class BomItemSerializer(InvenTreeModelSerializer): # Calculate "total stock" for the referenced sub_part # Calculate the "build_order_allocations" for the sub_part - queryset = queryset.annotate( + queryset = queryset.alias( total_stock=Coalesce( SubquerySum('sub_part__stock_items__quantity', filter=StockItem.IN_STOCK_FILTER), Decimal(0), @@ -661,13 +663,16 @@ class BomItemSerializer(InvenTreeModelSerializer): ), Decimal(0), output_field=models.DecimalField(), - ) - # build_order_allocations=Coalesce( - # SubquerySum('sub_part__stock_items__allocations__quantity'), - # ) + ), ) - # queryset = querty + # Calculate 'available_stock' based on previously annotated fields + queryset = queryset.annotate( + available_stock=ExpressionWrapper( + F('total_stock') - F('allocated_to_sales_orders') - F('allocated_to_build_orders'), + output_field=models.DecimalField(), + ) + ) return queryset @@ -741,9 +746,8 @@ class BomItemSerializer(InvenTreeModelSerializer): 'price_range', 'validated', - 'total_stock', - 'allocated_to_sales_orders', - 'allocated_to_build_orders', + # Annotated fields describing available quantity + 'available_stock', ] From e4ca638a2eedb8802e38ddbc86d83e3342dd5843 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 4 Apr 2022 23:52:45 +1000 Subject: [PATCH 03/15] Add field for substitute_stock (work in progress) --- InvenTree/part/serializers.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 5d62d5414b..9e393724fa 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -579,6 +579,7 @@ class BomItemSerializer(InvenTreeModelSerializer): # Annotated fields available_stock = serializers.FloatField(read_only=True) + substitute_stock = serializers.FloatField(read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. @@ -637,9 +638,13 @@ class BomItemSerializer(InvenTreeModelSerializer): # Calculate "total stock" for the referenced sub_part # Calculate the "build_order_allocations" for the sub_part + # Note that these fields are only aliased, not annotated queryset = queryset.alias( total_stock=Coalesce( - SubquerySum('sub_part__stock_items__quantity', filter=StockItem.IN_STOCK_FILTER), + SubquerySum( + 'sub_part__stock_items__quantity', + filter=StockItem.IN_STOCK_FILTER + ), Decimal(0), output_field=models.DecimalField(), ), @@ -674,6 +679,18 @@ class BomItemSerializer(InvenTreeModelSerializer): ) ) + # Extract similar information for any 'substitute' parts + queryset = queryset.annotate( + substitute_stock=Coalesce( + SubquerySum( + 'substitutes__part__stock_items__quantity', + filter=StockItem.IN_STOCK_FILTER, + ), + Decimal(0), + output_field=models.DecimalField(), + ) + ) + return queryset def get_purchase_price_range(self, obj): @@ -748,7 +765,7 @@ class BomItemSerializer(InvenTreeModelSerializer): # Annotated fields describing available quantity 'available_stock', - + 'substitute_stock', ] From 30a4c38eb792f97850df5188d6c87e85de27a3f6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 5 Apr 2022 21:12:43 +1000 Subject: [PATCH 04/15] Ensure queryset is properly annotated for BomItem detail --- InvenTree/part/api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index bab1bc6486..de6cd4a974 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -1819,6 +1819,15 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView): queryset = BomItem.objects.all() serializer_class = part_serializers.BomItemSerializer + def get_queryset(self, *args, **kwargs): + + queryset = super().get_queryset(*args, **kwargs) + + queryset = self.get_serializer_class().setup_eager_loading(queryset) + queryset = self.get_serializer_class().annotate_queryset(queryset) + + return queryset + class BomItemValidate(generics.UpdateAPIView): """ API endpoint for validating a BomItem """ From dc2da4bcb9108c4d02dfc362abe1fc924c0f9394 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 5 Apr 2022 21:24:57 +1000 Subject: [PATCH 05/15] BomItem API - improve annotation of available substitute stock quantity --- InvenTree/part/serializers.py | 45 +++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 9e393724fa..437d6507cb 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -579,7 +579,7 @@ class BomItemSerializer(InvenTreeModelSerializer): # Annotated fields available_stock = serializers.FloatField(read_only=True) - substitute_stock = serializers.FloatField(read_only=True) + available_substitute_stock = serializers.FloatField(read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. @@ -636,6 +636,12 @@ class BomItemSerializer(InvenTreeModelSerializer): available_stock = total_stock - build_order_allocations - sales_order_allocations """ + build_order_filter = Q(build__status__in=BuildStatus.ACTIVE_CODES) + sales_order_filter = Q( + line__order__status__in=SalesOrderStatus.OPEN, + shipment__shipment_date=None, + ) + # Calculate "total stock" for the referenced sub_part # Calculate the "build_order_allocations" for the sub_part # Note that these fields are only aliased, not annotated @@ -651,10 +657,7 @@ class BomItemSerializer(InvenTreeModelSerializer): allocated_to_sales_orders=Coalesce( SubquerySum( 'sub_part__stock_items__sales_order_allocations__quantity', - filter=Q( - line__order__status__in=SalesOrderStatus.OPEN, - shipment__shipment_date=None, - ) + filter=sales_order_filter, ), Decimal(0), output_field=models.DecimalField(), @@ -662,9 +665,7 @@ class BomItemSerializer(InvenTreeModelSerializer): allocated_to_build_orders=Coalesce( SubquerySum( 'sub_part__stock_items__allocations__quantity', - filter=Q( - build__status__in=BuildStatus.ACTIVE_CODES, - ), + filter=build_order_filter, ), Decimal(0), output_field=models.DecimalField(), @@ -680,7 +681,7 @@ class BomItemSerializer(InvenTreeModelSerializer): ) # Extract similar information for any 'substitute' parts - queryset = queryset.annotate( + queryset = queryset.alias( substitute_stock=Coalesce( SubquerySum( 'substitutes__part__stock_items__quantity', @@ -688,6 +689,30 @@ class BomItemSerializer(InvenTreeModelSerializer): ), Decimal(0), output_field=models.DecimalField(), + ), + substitute_build_allocations=Coalesce( + SubquerySum( + 'substitutes__part__stock_items__allocations__quantity', + filter=build_order_filter, + ), + Decimal(0), + output_field=models.DecimalField(), + ), + substitute_sales_allocations=Coalesce( + SubquerySum( + 'substitutes__part__stock_items__sales_order_allocations__quantity', + filter=sales_order_filter, + ), + Decimal(0), + output_field=models.DecimalField(), + ), + ) + + # Calculate 'available_variant_stock' field + queryset = queryset.annotate( + available_substitute_stock=ExpressionWrapper( + F('substitute_stock') - F('substitute_build_allocations') - F('substitute_sales_allocations'), + output_field=models.DecimalField(), ) ) @@ -765,7 +790,7 @@ class BomItemSerializer(InvenTreeModelSerializer): # Annotated fields describing available quantity 'available_stock', - 'substitute_stock', + 'available_substitute_stock', ] From 6aceb24e41d1c340d2db3fff67c91726d96ba9e8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Apr 2022 17:45:11 +1000 Subject: [PATCH 06/15] Include available substitute stock when calculating total availability in BOM table --- InvenTree/templates/js/translated/bom.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 93e1562a38..6de3cd2d15 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -798,17 +798,26 @@ function loadBomTable(table, options={}) { }); cols.push({ - field: 'sub_part_detail.stock', + field: 'available_stock', title: '{% trans "Available" %}', searchable: false, sortable: true, formatter: function(value, row) { var url = `/part/${row.sub_part_detail.pk}/?display=part-stock`; - var text = value; - if (value == null || value <= 0) { - text = `{% trans "No Stock" %}`; + // Calculate total "available" (unallocated) quantity + var total = row.available_stock + row.available_substitute_stock; + // var text = row.available_substitute_stock + row.available_stock; + + if (total <= 0) { + text = `{% trans "No Stock Available" %}`; + } else { + text = `${total}`; + + if (row.available_substitute_stock > 0) { + text += ``; + } } return renderLink(text, url); From acc6cb872900b22dd349bfa6c03c08e3ed83f2f7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Apr 2022 17:47:24 +1000 Subject: [PATCH 07/15] Fix calculation of "can_build" --- InvenTree/templates/js/translated/bom.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 6de3cd2d15..1e384bf734 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -911,8 +911,10 @@ function loadBomTable(table, options={}) { formatter: function(value, row) { var can_build = 0; + var available = row.available_stock + row.available_substitute_stock; + if (row.quantity > 0) { - can_build = row.sub_part_detail.stock / row.quantity; + can_build = available / row.quantity; } return +can_build.toFixed(2); @@ -923,11 +925,11 @@ function loadBomTable(table, options={}) { var cb_b = 0; if (rowA.quantity > 0) { - cb_a = rowA.sub_part_detail.stock / rowA.quantity; + cb_a = (rowA.available_stock + rowA.available_substitute_stock) / rowA.quantity; } if (rowB.quantity > 0) { - cb_b = rowB.sub_part_detail.stock / rowB.quantity; + cb_b = (rowB.available_stock + rowB.available_substitute_stock) / rowB.quantity; } return (cb_a > cb_b) ? 1 : -1; From f3075d2151668a2df6a57b64fa5c6cc9b3c5f79a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Apr 2022 18:48:23 +1000 Subject: [PATCH 08/15] Build table now also shows availability of substitute stock --- InvenTree/templates/js/translated/bom.js | 5 ++--- InvenTree/templates/js/translated/build.js | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 1e384bf734..6591068e9d 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -808,13 +808,12 @@ function loadBomTable(table, options={}) { // Calculate total "available" (unallocated) quantity var total = row.available_stock + row.available_substitute_stock; - // var text = row.available_substitute_stock + row.available_stock; + + var text = `${total}`; if (total <= 0) { text = `{% trans "No Stock Available" %}`; } else { - text = `${total}`; - if (row.available_substitute_stock > 0) { text += ``; } diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index d4db965ebd..ff1f475d5d 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1421,9 +1421,24 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { sortable: true, }, { - field: 'sub_part_detail.stock', + field: 'available_stock', title: '{% trans "Available" %}', sortable: true, + formatter: function(value, row) { + var total = row.available_stock + row.available_substitute_stock; + + var text = `${total}`; + + if (total <= 0) { + text = `{% trans "No Stock Available" %}`; + } else { + if (row.available_substitute_stock > 0) { + text += ``; + } + } + + return text; + } }, { field: 'allocated', From ba81e6caf9c6d88a8f43da1c260abb5fddb7abda Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Apr 2022 18:50:10 +1000 Subject: [PATCH 09/15] Style fixes --- InvenTree/part/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 437d6507cb..8bf3d77501 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -632,7 +632,6 @@ class BomItemSerializer(InvenTreeModelSerializer): """ Construct an "available stock" quantity: - available_stock = total_stock - build_order_allocations - sales_order_allocations """ From be9648cbc79603df98da0a919cac357d74b614d8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Apr 2022 19:22:20 +1000 Subject: [PATCH 10/15] Remove unused function which shadowed name of query annotation --- InvenTree/part/models.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index c493028d71..d67ff31c8c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2882,23 +2882,6 @@ class BomItem(models.Model, DataImportMixin): child=self.sub_part.full_name, n=decimal2string(self.quantity)) - def available_stock(self): - """ - Return the available stock items for the referenced sub_part - """ - - query = self.sub_part.stock_items.all() - - query = query.prefetch_related([ - 'sub_part__stock_items', - ]) - - query = query.filter(StockModels.StockItem.IN_STOCK_FILTER).aggregate( - available=Coalesce(Sum('quantity'), 0) - ) - - return query['available'] - def get_overage_quantity(self, quantity): """ Calculate overage quantity """ From 49f095b15cfb4f12512a4fec01ea455f2493cf9d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Apr 2022 19:37:16 +1000 Subject: [PATCH 11/15] Bump API version --- InvenTree/InvenTree/version.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 0a9d39225b..98957d7bba 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,14 @@ import common.models INVENTREE_SW_VERSION = "0.7.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 36 +INVENTREE_API_VERSION = 37 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v37 -> 2022-04-07 : https://github.com/inventree/InvenTree/pull/2806 + - Adds extra stock availability information to the BomItem serializer + v36 -> 2022-04-03 - Adds ability to filter part list endpoint by unallocated_stock argument From 8ab54c8e55c2a01e90fe6fd617fca5d8bb8e9a25 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 8 Apr 2022 21:39:41 +1000 Subject: [PATCH 12/15] Add more unit tests --- InvenTree/part/test_api.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index bea7154612..1d0eb812da 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -9,7 +9,7 @@ from rest_framework import status from rest_framework.test import APIClient from InvenTree.api_tester import InvenTreeAPITestCase -from InvenTree.status_codes import BuildStatus, StockStatus +from InvenTree.status_codes import BuildStatus, StockStatus, PurchaseOrderStatus from part.models import Part, PartCategory from part.models import BomItem, BomItemSubstitute @@ -578,7 +578,12 @@ class PartDetailTests(InvenTreeAPITestCase): 'part', 'location', 'bom', + 'company', 'test_templates', + 'manufacturer_part', + 'supplier_part', + 'order', + 'stock', ] roles = [ @@ -805,6 +810,35 @@ class PartDetailTests(InvenTreeAPITestCase): # And now check that the image has been set p = Part.objects.get(pk=pk) + def test_details(self): + """ + Test that the required details are available + """ + + url = reverse('api-part-detail', kwargs={'pk': 1}) + + data = self.get(url, expected_code=200).data + + # How many parts are 'on order' for this part? + lines = order.models.PurchaseOrderLineItem.objects.filter( + part__part__pk=1, + order__status__in=PurchaseOrderStatus.OPEN, + ) + + on_order = 0 + + # Calculate the "on_order" quantity by hand, + # to check it matches the API value + for line in lines: + on_order += line.quantity + on_order -= line.received + + self.assertEqual(on_order, data['ordering']) + + # Some other checks + self.assertEqual(data['in_stock'], 9000) + self.assertEqual(data['unallocated_stock'], 9000) + class PartAPIAggregationTest(InvenTreeAPITestCase): """ From 78ed5d9cc4f00fe253e3952f1242cfd6d873a114 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 8 Apr 2022 21:59:59 +1000 Subject: [PATCH 13/15] Some more API unit tests --- InvenTree/part/test_api.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 1d0eb812da..6ce55862d1 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -815,6 +815,8 @@ class PartDetailTests(InvenTreeAPITestCase): Test that the required details are available """ + p = Part.objects.get(pk=1) + url = reverse('api-part-detail', kwargs={'pk': 1}) data = self.get(url, expected_code=200).data @@ -834,6 +836,7 @@ class PartDetailTests(InvenTreeAPITestCase): on_order -= line.received self.assertEqual(on_order, data['ordering']) + self.assertEqual(on_order, p.on_order) # Some other checks self.assertEqual(data['in_stock'], 9000) @@ -1157,6 +1160,12 @@ class BomItemTest(InvenTreeAPITestCase): self.assertEqual(len(response.data), 1) self.assertEqual(response.data[0]['pk'], bom_item.pk) + # Each item in response should contain expected keys + for el in response.data: + + for key in ['available_stock', 'available_substitute_stock']: + self.assertTrue(key in el) + def test_get_bom_detail(self): """ Get the detail view for a single BomItem object @@ -1166,6 +1175,26 @@ class BomItemTest(InvenTreeAPITestCase): response = self.get(url, expected_code=200) + expected_values = [ + 'allow_variants', + 'inherited', + 'note', + 'optional', + 'overage', + 'pk', + 'part', + 'quantity', + 'reference', + 'sub_part', + 'substitutes', + 'validated', + 'available_stock', + 'available_substitute_stock', + ] + + for key in expected_values: + self.assertTrue(key in response.data) + self.assertEqual(int(float(response.data['quantity'])), 25) # Increase the quantity From 7af9e9123ec49c0e627e6f7418fba3f0ae9ffd7b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 9 Apr 2022 19:15:21 +1000 Subject: [PATCH 14/15] Pin bleach package version --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2979d3e9f3..0b0f95d864 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ # Please keep this list sorted -Django==3.2.12 # Django package +Django==3.2.12 # Django package +bleach==4.1.0 # HTML santization certifi # Certifi is (most likely) installed through one of the requirements above coreapi==2.3.0 # API documentation coverage==5.3 # Unit test coverage From b7937a4750558a256d2904e829743b186114ed88 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 9 Apr 2022 19:22:12 +1000 Subject: [PATCH 15/15] Add some more unit tests for BOM API endpoints --- InvenTree/part/test_api.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 6ce55862d1..5a8acbecd9 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -1382,6 +1382,21 @@ class BomItemTest(InvenTreeAPITestCase): response = self.get(url, expected_code=200) self.assertEqual(len(response.data), 5) + # The BomItem detail endpoint should now also reflect the substitute data + data = self.get( + reverse('api-bom-item-detail', kwargs={'pk': bom_item.pk}), + expected_code=200 + ).data + + # 5 substitute parts + self.assertEqual(len(data['substitutes']), 5) + + # 5 x 1,000 stock quantity + self.assertEqual(data['available_substitute_stock'], 5000) + + # 9,000 stock directly available + self.assertEqual(data['available_stock'], 9000) + def test_bom_item_uses(self): """ Tests for the 'uses' field