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:
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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."""
|
||||
|
Reference in New Issue
Block a user