diff --git a/src/backend/InvenTree/InvenTree/api.py b/src/backend/InvenTree/InvenTree/api.py
index 79173c6ab4..249091c9b0 100644
--- a/src/backend/InvenTree/InvenTree/api.py
+++ b/src/backend/InvenTree/InvenTree/api.py
@@ -594,17 +594,24 @@ class BulkUpdateMixin(BulkOperationMixin):
n = queryset.count()
+ instance_data = []
+
with transaction.atomic():
# Perform object update
# Note that we do not perform a bulk-update operation here,
# as we want to trigger any custom post_save methods on the model
+
+ # Run validation first
for instance in queryset:
serializer = self.get_serializer(instance, data=data, partial=True)
-
serializer.is_valid(raise_exception=True)
serializer.save()
- return Response({'success': f'Updated {n} items'}, status=200)
+ instance_data.append(serializer.data)
+
+ return Response(
+ {'success': f'Updated {n} items', 'items': instance_data}, status=200
+ )
class ParameterListMixin:
diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index 5b266f89e8..9a0cd731cd 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 462
+INVENTREE_API_VERSION = 463
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
+v463 -> 2026-03-12 : https://github.com/inventree/InvenTree/pull/11499
+ - Allow "bulk update" actions against StockItem endpoint
+
v462 -> 2026-03-12 : https://github.com/inventree/InvenTree/pull/11497
- Allow "ScheduledTask" API endpoint to be filtered by "name" field
diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py
index bef906f6e5..522bad77e2 100644
--- a/src/backend/InvenTree/stock/api.py
+++ b/src/backend/InvenTree/stock/api.py
@@ -1050,7 +1050,11 @@ class StockOutputOptions(OutputConfiguration):
class StockList(
- DataExportViewMixin, StockApiMixin, OutputOptionsMixin, ListCreateDestroyAPIView
+ DataExportViewMixin,
+ BulkUpdateMixin,
+ StockApiMixin,
+ OutputOptionsMixin,
+ ListCreateDestroyAPIView,
):
"""API endpoint for list view of Stock objects.
diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py
index cbaea852f9..c57a8e6de0 100644
--- a/src/backend/InvenTree/stock/test_api.py
+++ b/src/backend/InvenTree/stock/test_api.py
@@ -2006,6 +2006,33 @@ class StockItemTest(StockAPITestCase):
self.assertEqual(tracking.deltas['status'], StockStatus.OK.value)
self.assertEqual(tracking.deltas['status_logical'], StockStatus.OK.value)
+ def test_bulk_batch_change(self):
+ """Test that we can bulk-change batch code for a set of stock items."""
+ url = reverse('api-stock-list')
+
+ # Find the first 10 stock items
+ items = StockItem.objects.all()[:10]
+ self.assertEqual(len(items), 10)
+
+ response = self.patch(
+ url,
+ data={'items': [item.pk for item in items], 'batch': 'NEW-BATCH-CODE'},
+ max_query_count=300,
+ )
+
+ data = response.data
+
+ self.assertEqual(data['success'], 'Updated 10 items')
+ self.assertEqual(len(data['items']), 10)
+
+ for item in data['items']:
+ self.assertEqual(item['batch'], 'NEW-BATCH-CODE')
+
+ # Check database items also
+ for item in items:
+ item.refresh_from_db()
+ self.assertEqual(item.batch, 'NEW-BATCH-CODE')
+
class StocktakeTest(StockAPITestCase):
"""Series of tests for the Stocktake API."""
diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx
index 67bbc51327..3ab80b7543 100644
--- a/src/frontend/src/forms/StockForms.tsx
+++ b/src/frontend/src/forms/StockForms.tsx
@@ -51,6 +51,7 @@ import { RenderStockLocation } from '../components/render/Stock';
import { InvenTreeIcon } from '../functions/icons';
import {
useApiFormModal,
+ useBulkEditApiFormModal,
useCreateApiFormModal,
useDeleteApiFormModal
} from '../hooks/UseForm';
@@ -1375,6 +1376,38 @@ export function useChangeStockStatus(props: StockOperationProps) {
});
}
+export function useChangeStockBatchCode(props: StockOperationProps) {
+ // Return a common batch code value if all items share the same batch code, otherwise return undefined
+ const batchCode = useMemo(() => {
+ const batchCodeValues = new Set(
+ props.items?.filter((item) => item.batch).map((item) => item.batch)
+ );
+
+ if (batchCodeValues.size === 1) {
+ return batchCodeValues.values().next().value;
+ }
+
+ return undefined;
+ }, [props.items]);
+
+ return useBulkEditApiFormModal({
+ url: ApiEndpoints.stock_item_list,
+ items: props.items?.map((item: any) => item.pk) ?? [],
+ title: t`Change Batch Code`,
+ preFormContent: (
+
+ {t`Change batch code for the selected stock items`}
+
+ ),
+ fields: {
+ batch: {
+ value: batchCode
+ }
+ },
+ onFormSuccess: props.refresh
+ });
+}
+
export function useMergeStockItem(props: StockOperationProps) {
return useStockOperationModal({
...props,
diff --git a/src/frontend/src/hooks/UseStockAdjustActions.tsx b/src/frontend/src/hooks/UseStockAdjustActions.tsx
index f0450bf0b3..9fe864a25a 100644
--- a/src/frontend/src/hooks/UseStockAdjustActions.tsx
+++ b/src/frontend/src/hooks/UseStockAdjustActions.tsx
@@ -8,6 +8,7 @@ import { ActionDropdown } from '../components/items/ActionDropdown';
import {
useAddStockItem,
useAssignStockItem,
+ useChangeStockBatchCode,
useChangeStockStatus,
useCountStockItem,
useDeleteStockItem,
@@ -26,6 +27,7 @@ interface StockAdjustActionProps {
assign?: boolean;
count?: boolean;
changeStatus?: boolean;
+ changeBatch?: boolean;
delete?: boolean;
merge?: boolean;
remove?: boolean;
@@ -55,6 +57,7 @@ export function useStockAdjustActions(
const addStock = useAddStockItem(props.formProps);
const assignStock = useAssignStockItem(props.formProps);
const changeStatus = useChangeStockStatus(props.formProps);
+ const changeBatch = useChangeStockBatchCode(props.formProps);
const countStock = useCountStockItem(props.formProps);
const deleteStock = useDeleteStockItem(props.formProps);
const mergeStock = useMergeStockItem(props.formProps);
@@ -74,6 +77,7 @@ export function useStockAdjustActions(
props.assign != false && modals.push(assignStock);
props.count != false && modals.push(countStock);
props.changeStatus != false && modals.push(changeStatus);
+ props.changeBatch != false && modals.push(changeBatch);
props.merge != false && modals.push(mergeStock);
props.remove != false && modals.push(removeStock);
props.transfer != false && modals.push(transferStock);
@@ -153,6 +157,16 @@ export function useStockAdjustActions(
}
});
+ props.changeBatch != false &&
+ menuActions.push({
+ name: t`Change Batch Code`,
+ icon: ,
+ tooltip: t`Change batch code of selected stock items`,
+ onClick: () => {
+ changeBatch.open();
+ }
+ });
+
props.assign != false &&
menuActions.push({
name: t`Assign Stock`,
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 22747010dd..7551ba964f 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -1118,6 +1118,7 @@ export default function PartDetail() {
const stockAdjustActions = useStockAdjustActions({
formProps: stockOperationProps,
merge: false,
+ changeBatch: false,
enabled: true
});
diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx
index 1cdd66d5b5..8d25eefbed 100644
--- a/src/frontend/src/pages/stock/LocationDetail.tsx
+++ b/src/frontend/src/pages/stock/LocationDetail.tsx
@@ -307,6 +307,7 @@ export default function Stock() {
const stockAdjustActions = useStockAdjustActions({
formProps: stockOperationProps,
enabled: true,
+ changeBatch: false,
delete: false,
merge: false,
assign: false
diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx
index 6b39b83964..c508083616 100644
--- a/src/frontend/src/pages/stock/StockDetail.tsx
+++ b/src/frontend/src/pages/stock/StockDetail.tsx
@@ -754,6 +754,7 @@ export default function StockDetail() {
const stockAdjustActions = useStockAdjustActions({
formProps: stockOperationProps,
delete: false,
+ changeBatch: false,
assign: !!stockitem.in_stock && stockitem.part_detail?.salable,
return: !!stockitem.consumed_by || !!stockitem.customer,
merge: false
diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx
index de0d125548..04d37dab84 100644
--- a/src/frontend/src/tables/stock/StockItemTable.tsx
+++ b/src/frontend/src/tables/stock/StockItemTable.tsx
@@ -428,7 +428,8 @@ export function StockItemTable({
const stockAdjustActions = useStockAdjustActions({
formProps: stockOperationProps,
- return: allowReturn
+ return: allowReturn,
+ changeBatch: true
});
const tableActions = useMemo(() => {