Skip to content

Commit 6ca102c

Browse files
Allow bulk edit of stock batch code (#11499)
* Allow bulk edit of stock batch code Closes #10817 * Bump API version * Add unit test
1 parent bc0ab35 commit 6ca102c

File tree

10 files changed

+97
-5
lines changed

10 files changed

+97
-5
lines changed

src/backend/InvenTree/InvenTree/api.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,17 +594,24 @@ def patch(self, request, *args, **kwargs):
594594

595595
n = queryset.count()
596596

597+
instance_data = []
598+
597599
with transaction.atomic():
598600
# Perform object update
599601
# Note that we do not perform a bulk-update operation here,
600602
# as we want to trigger any custom post_save methods on the model
603+
604+
# Run validation first
601605
for instance in queryset:
602606
serializer = self.get_serializer(instance, data=data, partial=True)
603-
604607
serializer.is_valid(raise_exception=True)
605608
serializer.save()
606609

607-
return Response({'success': f'Updated {n} items'}, status=200)
610+
instance_data.append(serializer.data)
611+
612+
return Response(
613+
{'success': f'Updated {n} items', 'items': instance_data}, status=200
614+
)
608615

609616

610617
class ParameterListMixin:

src/backend/InvenTree/InvenTree/api_version.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"""InvenTree API version information."""
22

33
# InvenTree API version
4-
INVENTREE_API_VERSION = 462
4+
INVENTREE_API_VERSION = 463
55
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
66

77
INVENTREE_API_TEXT = """
88
9+
v463 -> 2026-03-12 : https://github.com/inventree/InvenTree/pull/11499
10+
- Allow "bulk update" actions against StockItem endpoint
11+
912
v462 -> 2026-03-12 : https://github.com/inventree/InvenTree/pull/11497
1013
- Allow "ScheduledTask" API endpoint to be filtered by "name" field
1114

src/backend/InvenTree/stock/api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,11 @@ class StockOutputOptions(OutputConfiguration):
10501050

10511051

10521052
class StockList(
1053-
DataExportViewMixin, StockApiMixin, OutputOptionsMixin, ListCreateDestroyAPIView
1053+
DataExportViewMixin,
1054+
BulkUpdateMixin,
1055+
StockApiMixin,
1056+
OutputOptionsMixin,
1057+
ListCreateDestroyAPIView,
10541058
):
10551059
"""API endpoint for list view of Stock objects.
10561060

src/backend/InvenTree/stock/test_api.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2006,6 +2006,33 @@ def test_set_custom_status(self):
20062006
self.assertEqual(tracking.deltas['status'], StockStatus.OK.value)
20072007
self.assertEqual(tracking.deltas['status_logical'], StockStatus.OK.value)
20082008

2009+
def test_bulk_batch_change(self):
2010+
"""Test that we can bulk-change batch code for a set of stock items."""
2011+
url = reverse('api-stock-list')
2012+
2013+
# Find the first 10 stock items
2014+
items = StockItem.objects.all()[:10]
2015+
self.assertEqual(len(items), 10)
2016+
2017+
response = self.patch(
2018+
url,
2019+
data={'items': [item.pk for item in items], 'batch': 'NEW-BATCH-CODE'},
2020+
max_query_count=300,
2021+
)
2022+
2023+
data = response.data
2024+
2025+
self.assertEqual(data['success'], 'Updated 10 items')
2026+
self.assertEqual(len(data['items']), 10)
2027+
2028+
for item in data['items']:
2029+
self.assertEqual(item['batch'], 'NEW-BATCH-CODE')
2030+
2031+
# Check database items also
2032+
for item in items:
2033+
item.refresh_from_db()
2034+
self.assertEqual(item.batch, 'NEW-BATCH-CODE')
2035+
20092036

20102037
class StocktakeTest(StockAPITestCase):
20112038
"""Series of tests for the Stocktake API."""

src/frontend/src/forms/StockForms.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { RenderStockLocation } from '../components/render/Stock';
5151
import { InvenTreeIcon } from '../functions/icons';
5252
import {
5353
useApiFormModal,
54+
useBulkEditApiFormModal,
5455
useCreateApiFormModal,
5556
useDeleteApiFormModal
5657
} from '../hooks/UseForm';
@@ -1375,6 +1376,38 @@ export function useChangeStockStatus(props: StockOperationProps) {
13751376
});
13761377
}
13771378

1379+
export function useChangeStockBatchCode(props: StockOperationProps) {
1380+
// Return a common batch code value if all items share the same batch code, otherwise return undefined
1381+
const batchCode = useMemo(() => {
1382+
const batchCodeValues = new Set(
1383+
props.items?.filter((item) => item.batch).map((item) => item.batch)
1384+
);
1385+
1386+
if (batchCodeValues.size === 1) {
1387+
return batchCodeValues.values().next().value;
1388+
}
1389+
1390+
return undefined;
1391+
}, [props.items]);
1392+
1393+
return useBulkEditApiFormModal({
1394+
url: ApiEndpoints.stock_item_list,
1395+
items: props.items?.map((item: any) => item.pk) ?? [],
1396+
title: t`Change Batch Code`,
1397+
preFormContent: (
1398+
<Alert color='blue'>
1399+
{t`Change batch code for the selected stock items`}
1400+
</Alert>
1401+
),
1402+
fields: {
1403+
batch: {
1404+
value: batchCode
1405+
}
1406+
},
1407+
onFormSuccess: props.refresh
1408+
});
1409+
}
1410+
13781411
export function useMergeStockItem(props: StockOperationProps) {
13791412
return useStockOperationModal({
13801413
...props,

src/frontend/src/hooks/UseStockAdjustActions.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ActionDropdown } from '../components/items/ActionDropdown';
88
import {
99
useAddStockItem,
1010
useAssignStockItem,
11+
useChangeStockBatchCode,
1112
useChangeStockStatus,
1213
useCountStockItem,
1314
useDeleteStockItem,
@@ -26,6 +27,7 @@ interface StockAdjustActionProps {
2627
assign?: boolean;
2728
count?: boolean;
2829
changeStatus?: boolean;
30+
changeBatch?: boolean;
2931
delete?: boolean;
3032
merge?: boolean;
3133
remove?: boolean;
@@ -55,6 +57,7 @@ export function useStockAdjustActions(
5557
const addStock = useAddStockItem(props.formProps);
5658
const assignStock = useAssignStockItem(props.formProps);
5759
const changeStatus = useChangeStockStatus(props.formProps);
60+
const changeBatch = useChangeStockBatchCode(props.formProps);
5861
const countStock = useCountStockItem(props.formProps);
5962
const deleteStock = useDeleteStockItem(props.formProps);
6063
const mergeStock = useMergeStockItem(props.formProps);
@@ -74,6 +77,7 @@ export function useStockAdjustActions(
7477
props.assign != false && modals.push(assignStock);
7578
props.count != false && modals.push(countStock);
7679
props.changeStatus != false && modals.push(changeStatus);
80+
props.changeBatch != false && modals.push(changeBatch);
7781
props.merge != false && modals.push(mergeStock);
7882
props.remove != false && modals.push(removeStock);
7983
props.transfer != false && modals.push(transferStock);
@@ -153,6 +157,16 @@ export function useStockAdjustActions(
153157
}
154158
});
155159

160+
props.changeBatch != false &&
161+
menuActions.push({
162+
name: t`Change Batch Code`,
163+
icon: <InvenTreeIcon icon='batch' iconProps={{ color: 'blue' }} />,
164+
tooltip: t`Change batch code of selected stock items`,
165+
onClick: () => {
166+
changeBatch.open();
167+
}
168+
});
169+
156170
props.assign != false &&
157171
menuActions.push({
158172
name: t`Assign Stock`,

src/frontend/src/pages/part/PartDetail.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,7 @@ export default function PartDetail() {
11181118
const stockAdjustActions = useStockAdjustActions({
11191119
formProps: stockOperationProps,
11201120
merge: false,
1121+
changeBatch: false,
11211122
enabled: true
11221123
});
11231124

src/frontend/src/pages/stock/LocationDetail.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ export default function Stock() {
307307
const stockAdjustActions = useStockAdjustActions({
308308
formProps: stockOperationProps,
309309
enabled: true,
310+
changeBatch: false,
310311
delete: false,
311312
merge: false,
312313
assign: false

src/frontend/src/pages/stock/StockDetail.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ export default function StockDetail() {
754754
const stockAdjustActions = useStockAdjustActions({
755755
formProps: stockOperationProps,
756756
delete: false,
757+
changeBatch: false,
757758
assign: !!stockitem.in_stock && stockitem.part_detail?.salable,
758759
return: !!stockitem.consumed_by || !!stockitem.customer,
759760
merge: false

src/frontend/src/tables/stock/StockItemTable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,8 @@ export function StockItemTable({
428428

429429
const stockAdjustActions = useStockAdjustActions({
430430
formProps: stockOperationProps,
431-
return: allowReturn
431+
return: allowReturn,
432+
changeBatch: true
432433
});
433434

434435
const tableActions = useMemo(() => {

0 commit comments

Comments
 (0)