2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

[UI] Add "can build" part info (#9798)

* Add proper serializer to PartRequirements API endpoint

* Add API endpoint

* Display "can_build" quantity

* Add simple playwright tests

* Bump API version

* Updated docs

* Fix formatting

* Consolidate field names

- Match field names to the PartSerializer

* Adjust frontend

* Add "can_build" to BuildDetail page

* Tweak BuildDetail

* Hide until load

* serializer fixes
This commit is contained in:
Oliver
2025-06-18 10:17:39 +10:00
committed by GitHub
parent fe4038205f
commit 62aef238f0
8 changed files with 190 additions and 49 deletions

View File

@ -1,12 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 349
INVENTREE_API_VERSION = 350
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v350 -> 2025-06-17 : https://github.com/inventree/InvenTree/pull/9798
- Adds "can_build" field to the part requirements API endpoint
- Remove "allocated" and "required" fields from the part requirements API endpoint
- Add detailed serializer to the part requirements API endpoint
v349 -> 2025-06-13 : https://github.com/inventree/InvenTree/pull/9574
- Remove the 'create_child_builds' flag from the BuildOrder creation API endpoint

View File

@ -754,38 +754,13 @@ class PartRequirements(RetrieveAPI):
- Sales Orders
- Build Orders
- Total requirements
- How many of this part can be assembled with available stock
As this data is somewhat complex to calculate, is it not included in the default API
"""
queryset = Part.objects.all()
serializer_class = EmptySerializer
def retrieve(self, request, *args, **kwargs):
"""Construct a response detailing Part requirements."""
part = self.get_object()
data = {
'available_stock': part.available_stock,
'on_order': part.on_order,
'required_build_order_quantity': part.required_build_order_quantity(),
'allocated_build_order_quantity': part.build_order_allocation_count(),
'required_sales_order_quantity': part.required_sales_order_quantity(),
'allocated_sales_order_quantity': part.sales_order_allocation_count(
pending=True
),
}
data['allocated'] = (
data['allocated_build_order_quantity']
+ data['allocated_sales_order_quantity']
)
data['required'] = (
data['required_build_order_quantity']
+ data['required_sales_order_quantity']
)
return Response(data)
serializer_class = part_serializers.PartRequirementsSerializer
class PartPricingDetail(RetrieveUpdateAPI):

View File

@ -98,10 +98,6 @@ class CategorySerializer(
if not path_detail and not isGeneratingSchema():
self.fields.pop('path', None)
def get_starred(self, category) -> bool:
"""Return True if the category is directly "starred" by the current user."""
return category in self.context.get('starred_categories', [])
@staticmethod
def annotate_queryset(queryset):
"""Annotate extra information to the queryset."""
@ -137,6 +133,10 @@ class CategorySerializer(
starred = serializers.SerializerMethodField()
def get_starred(self, category) -> bool:
"""Return True if the category is directly "starred" by the current user."""
return category in self.context.get('starred_categories', [])
path = serializers.ListField(
child=serializers.DictField(),
source='get_path',
@ -1223,6 +1223,73 @@ class PartSerializer(
return self.instance
class PartRequirementsSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for Part requirements."""
class Meta:
"""Metaclass options."""
model = Part
fields = [
'total_stock',
'unallocated_stock',
'can_build',
'ordering',
'building',
'scheduled_to_build',
'required_for_build_orders',
'allocated_to_build_orders',
'required_for_sales_orders',
'allocated_to_sales_orders',
]
total_stock = serializers.FloatField(read_only=True, label=_('Total Stock'))
unallocated_stock = serializers.FloatField(
source='available_stock', read_only=True, label=_('Available Stock')
)
can_build = serializers.FloatField(read_only=True, label=_('Can Build'))
ordering = serializers.FloatField(
source='on_order', read_only=True, label=_('On Order')
)
building = serializers.FloatField(
read_only=True, label=_('In Production'), source='quantity_in_production'
)
scheduled_to_build = serializers.FloatField(
read_only=True, label=_('Scheduled to Build'), source='quantity_being_built'
)
required_for_build_orders = serializers.FloatField(
source='required_build_order_quantity',
read_only=True,
label=_('Required for Build Orders'),
)
allocated_to_build_orders = serializers.FloatField(
read_only=True,
label=_('Allocated to Build Orders'),
source='build_order_allocation_count',
)
required_for_sales_orders = serializers.FloatField(
source='required_sales_order_quantity',
read_only=True,
label=_('Required for Sales Orders'),
)
allocated_to_sales_orders = serializers.SerializerMethodField(
read_only=True, label=_('Allocated to Sales Orders')
)
def get_allocated_to_sales_orders(self, part) -> float:
"""Return the allocated sales order quantity."""
return part.sales_order_allocation_count(pending=True)
class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for the PartStocktake model."""

View File

@ -1843,6 +1843,30 @@ class PartDetailTests(PartImageTestMixin, PartAPITestBase):
self.assertIn('category_path', response.data)
self.assertEqual(len(response.data['category_path']), 2)
def test_part_requirements(self):
"""Unit test for the "PartRequirements" API endpoint."""
url = reverse('api-part-requirements', kwargs={'pk': Part.objects.first().pk})
# Get the requirements for part 1
response = self.get(url, expected_code=200)
# Check that the response contains the expected fields
expected_fields = [
'total_stock',
'unallocated_stock',
'can_build',
'ordering',
'building',
'scheduled_to_build',
'required_for_build_orders',
'allocated_to_build_orders',
'required_for_sales_orders',
'allocated_to_sales_orders',
]
for field in expected_fields:
self.assertIn(field, response.data)
class PartListTests(PartAPITestBase):
"""Unit tests for the Part List API endpoint."""