mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-01 03:00:54 +00:00
[API] Return stock item list when creating multiple items (#9857)
* Return stock item information when serializing an existing item * Handle stock item creation * Commonize response * Provide build items in response * Formalize returned data type * Fix unit test
This commit is contained in:
@ -1,12 +1,17 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 357
|
||||
INVENTREE_API_VERSION = 358
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v358 -> 2025-06-25 : https://github.com/inventree/InvenTree/pull/9857
|
||||
- Provide list of generated stock items against "StockItemSerialize" API endpoint
|
||||
- Provide list of generated stock items against "StockList" API endpoint
|
||||
- Provide list of generated stock items against "BuildOutputCreate" API endpoint
|
||||
|
||||
v357 -> 2025-06-25 : https://github.com/inventree/InvenTree/pull/9856
|
||||
- Adds "units" field to PluginSetting API endpoints
|
||||
|
||||
|
@ -8,13 +8,15 @@ from django.urls import include, path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_filters import rest_framework as rest_filters
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
import build.serializers
|
||||
import common.models
|
||||
import part.models as part_models
|
||||
import stock.serializers
|
||||
from build.models import Build, BuildItem, BuildLine
|
||||
from build.status_codes import BuildStatus, BuildStatusGroups
|
||||
from data_exporter.mixins import DataExportViewMixin
|
||||
@ -647,6 +649,20 @@ class BuildOutputCreate(BuildOrderContextMixin, CreateAPI):
|
||||
|
||||
serializer_class = build.serializers.BuildOutputCreateSerializer
|
||||
|
||||
@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)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Create the build output(s)
|
||||
outputs = serializer.save()
|
||||
|
||||
response = stock.serializers.StockItemSerializer(outputs, many=True)
|
||||
|
||||
# Return the created outputs
|
||||
return Response(response.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class BuildOutputScrap(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for scrapping build output(s)."""
|
||||
@ -705,7 +721,7 @@ class BuildAutoAllocate(BuildOrderContextMixin, CreateAPI):
|
||||
- Only looks at 'untracked' parts
|
||||
- If stock exists in a single location, easy!
|
||||
- If user decides that stock items are "fungible", allocate against multiple stock items
|
||||
- If the user wants to, allocate substite parts if the primary parts are not available.
|
||||
- If the user wants to, allocate substitute parts if the primary parts are not available.
|
||||
"""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
@ -860,10 +860,10 @@ class Build(
|
||||
allocations.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def create_build_output(self, quantity, **kwargs):
|
||||
def create_build_output(self, quantity, **kwargs) -> list[stock.models.StockItem]:
|
||||
"""Create a new build output against this BuildOrder.
|
||||
|
||||
Args:
|
||||
Arguments:
|
||||
quantity: The quantity of the item to produce
|
||||
|
||||
Kwargs:
|
||||
@ -871,6 +871,9 @@ class Build(
|
||||
serials: Serial numbers
|
||||
location: Override location
|
||||
auto_allocate: Automatically allocate stock with matching serial numbers
|
||||
|
||||
Returns:
|
||||
A list of the created output (StockItem) objects.
|
||||
"""
|
||||
trackable_parts = self.part.get_trackable_parts()
|
||||
|
||||
@ -895,6 +898,8 @@ class Build(
|
||||
'serials': _('Serial numbers must be provided for trackable parts')
|
||||
})
|
||||
|
||||
outputs = []
|
||||
|
||||
# We are generating multiple serialized outputs
|
||||
if serials:
|
||||
"""Create multiple build outputs with a single quantity of 1."""
|
||||
@ -989,10 +994,14 @@ class Build(
|
||||
},
|
||||
)
|
||||
|
||||
outputs = [output]
|
||||
|
||||
if self.status == BuildStatus.PENDING:
|
||||
self.status = BuildStatus.PRODUCTION.value
|
||||
self.save()
|
||||
|
||||
return outputs
|
||||
|
||||
@transaction.atomic
|
||||
def delete_output(self, output):
|
||||
"""Remove a build output from the database.
|
||||
|
@ -421,7 +421,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
request = self.context.get('request')
|
||||
build = self.get_build()
|
||||
|
||||
build.create_build_output(
|
||||
return build.create_build_output(
|
||||
data['quantity'],
|
||||
serials=self.serials,
|
||||
batch=data.get('batch_code', ''),
|
||||
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_filters import rest_framework as rest_filters
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from rest_framework import status
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
@ -120,6 +120,21 @@ class StockItemSerialize(StockItemContextMixin, CreateAPI):
|
||||
|
||||
serializer_class = StockSerializers.SerializeStockItemSerializer
|
||||
|
||||
@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)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Perform the actual serialization step
|
||||
items = serializer.save()
|
||||
|
||||
response = StockSerializers.StockItemSerializer(
|
||||
items, many=True, context=self.get_serializer_context()
|
||||
)
|
||||
|
||||
return Response(response.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class StockItemInstall(StockItemContextMixin, CreateAPI):
|
||||
"""API endpoint for installing a particular stock item into this stock item.
|
||||
@ -1010,6 +1025,10 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView):
|
||||
# Check if a set of serial numbers was provided
|
||||
serial_numbers = data.pop('serial_numbers', '')
|
||||
|
||||
# Exclude 'serial' from submitted data
|
||||
# We use 'serial_numbers' for item creation
|
||||
data.pop('serial', None)
|
||||
|
||||
# Check if the supplier_part has a package size defined, which is not 1
|
||||
if supplier_part_id := data.get('supplier_part', None):
|
||||
try:
|
||||
@ -1097,10 +1116,6 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView):
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Exclude 'serial' from submitted data
|
||||
# We use 'serial_numbers' for item creation
|
||||
serializer.validated_data.pop('serial', None)
|
||||
|
||||
# Extract location information
|
||||
location = serializer.validated_data.get('location', None)
|
||||
|
||||
@ -1127,7 +1142,11 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView):
|
||||
|
||||
StockItemTracking.objects.bulk_create(tracking)
|
||||
|
||||
response_data = {'quantity': quantity, 'serial_numbers': serials}
|
||||
response = StockSerializers.StockItemSerializer(
|
||||
items, many=True, context=self.get_serializer_context()
|
||||
)
|
||||
|
||||
response_data = response.data
|
||||
|
||||
else:
|
||||
# Create a single StockItem object
|
||||
|
@ -1699,23 +1699,33 @@ class StockItem(
|
||||
return entry
|
||||
|
||||
@transaction.atomic
|
||||
def serializeStock(self, quantity, serials, user, notes='', location=None):
|
||||
def serializeStock(
|
||||
self,
|
||||
quantity: int,
|
||||
serials: list[str],
|
||||
user: User,
|
||||
notes: str = '',
|
||||
location: Optional[StockLocation] = None,
|
||||
):
|
||||
"""Split this stock item into unique serial numbers.
|
||||
|
||||
- Quantity can be less than or equal to the quantity of the stock item
|
||||
- Number of serial numbers must match the quantity
|
||||
- Provided serial numbers must not already be in use
|
||||
|
||||
Args:
|
||||
Arguments:
|
||||
quantity: Number of items to serialize (integer)
|
||||
serials: List of serial numbers
|
||||
user: User object associated with action
|
||||
notes: Optional notes for tracking
|
||||
location: If specified, serialized items will be placed in the given location
|
||||
|
||||
Returns:
|
||||
List of newly created StockItem objects, each with a unique serial number.
|
||||
"""
|
||||
# Cannot serialize stock that is already serialized!
|
||||
if self.serialized:
|
||||
return
|
||||
return None
|
||||
|
||||
if not self.part.trackable:
|
||||
raise ValidationError({'part': _('Part is not set as trackable')})
|
||||
@ -1804,6 +1814,8 @@ class StockItem(
|
||||
stock.tasks.rebuild_stock_item_tree, tree_id=self.tree_id, group='stock'
|
||||
)
|
||||
|
||||
return items
|
||||
|
||||
@transaction.atomic
|
||||
def copyHistoryFrom(self, other):
|
||||
"""Copy stock history from another StockItem."""
|
||||
|
@ -790,8 +790,12 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
||||
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""Serialize stock item."""
|
||||
def save(self) -> list[StockItem]:
|
||||
"""Serialize the provided StockItem.
|
||||
|
||||
Returns:
|
||||
A list of StockItem objects that were created as a result of the serialization.
|
||||
"""
|
||||
item = self.context['item']
|
||||
request = self.context.get('request')
|
||||
user = request.user if request else None
|
||||
@ -805,12 +809,15 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
||||
part=item.part,
|
||||
)
|
||||
|
||||
item.serializeStock(
|
||||
data['quantity'],
|
||||
serials,
|
||||
user,
|
||||
notes=data.get('notes', ''),
|
||||
location=data['destination'],
|
||||
return (
|
||||
item.serializeStock(
|
||||
data['quantity'],
|
||||
serials,
|
||||
user,
|
||||
notes=data.get('notes', ''),
|
||||
location=data['destination'],
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
|
||||
|
@ -1142,7 +1142,7 @@ class CustomStockItemStatusTest(StockAPITestCase):
|
||||
self.assertEqual(response.data['status'], self.status2.logical_key)
|
||||
self.assertEqual(response.data['status_custom_key'], self.status2.key)
|
||||
|
||||
# Try if status_custom_key is rewrite with status bying set
|
||||
# Try with custom status code
|
||||
response = self.patch(
|
||||
reverse('api-stock-detail', kwargs={'pk': pk}),
|
||||
{'status': self.status.logical_key},
|
||||
@ -1399,22 +1399,20 @@ class StockItemTest(StockAPITestCase):
|
||||
)
|
||||
|
||||
data = response.data
|
||||
|
||||
self.assertEqual(data['quantity'], 10)
|
||||
sn = data['serial_numbers']
|
||||
self.assertEqual(len(data), 10)
|
||||
serials = [item['serial'] for item in data]
|
||||
|
||||
# Check that each serial number was created
|
||||
for i in range(1, 11):
|
||||
self.assertIn(str(i), sn)
|
||||
self.assertIn(str(i), serials)
|
||||
|
||||
# Check the unique stock item has been created
|
||||
|
||||
item = StockItem.objects.get(part=trackable_part, serial=str(i))
|
||||
for item in data:
|
||||
item = StockItem.objects.get(pk=item['pk'])
|
||||
|
||||
# Item location should have been set automatically
|
||||
self.assertIsNotNone(item.location)
|
||||
|
||||
self.assertEqual(str(i), item.serial)
|
||||
self.assertIn(item.serial, serials)
|
||||
|
||||
# There now should be 10 unique stock entries for this part
|
||||
self.assertEqual(trackable_part.stock_entries().count(), 10)
|
||||
|
Reference in New Issue
Block a user