Implement duplicate action for stock item

This commit is contained in:
Oliver Walters 2026-07-04 14:18:46 +00:00
parent ebabc3359f
commit 427eb6d4b3
4 changed files with 74 additions and 2 deletions

View File

@ -1250,9 +1250,28 @@ class StockList(
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
# Extract 'duplicate' options (if provided) - these are not valid model fields
duplicate = serializer.validated_data.pop('duplicate', None)
# Extract location information
location = serializer.validated_data.get('location', None)
def apply_duplicate_options(item):
"""Apply any provided 'duplicate' options to a newly created StockItem."""
if not duplicate:
return
original = duplicate['original']
if duplicate.get('copy_notes', True):
item.copy_notes_from(original)
if duplicate.get('copy_history', False):
item.copyHistoryFrom(original)
if duplicate.get('copy_tests', False):
item.copyTestResultsFrom(original)
with transaction.atomic():
if serials:
# Create multiple serialized StockItem objects
@ -1268,6 +1287,8 @@ class StockList(
item.set_status(status_value)
item.save()
apply_duplicate_options(item)
if entry := item.add_tracking_entry(
StockHistoryCode.CREATED,
user,
@ -1300,6 +1321,8 @@ class StockList(
item.save(user=user)
item.refresh_from_db()
apply_duplicate_options(item)
response_data = [
StockSerializers.StockItemSerializer(
item, context=self.get_serializer_context()

View File

@ -324,6 +324,8 @@ class StockItemSerializer(
export_exclude_fields = ['tags', 'tracking_items']
SKIP_CREATE_FIELDS = ['duplicate']
export_child_fields = [
'part_detail.name',
'part_detail.description',
@ -380,6 +382,7 @@ class StockItemSerializer(
'purchase_price_currency',
'use_pack_size',
'serial_numbers',
'duplicate',
# Annotated fields
'allocated',
'expired',
@ -467,6 +470,28 @@ class StockItemSerializer(
help_text=_('Enter serial numbers for new items'),
)
# Extra field used only for creation of a new StockItem instance
duplicate = InvenTree.serializers.DuplicateOptionsSerializer(
StockItem.objects.all(),
label=_('Duplicate Stock Item'),
help_text=_('Copy initial data from another stock item'),
copy_notes=True,
copy_fields=[
{
'name': 'copy_tests',
'label': _('Copy Test Results'),
'help_text': _('Copy test results from the original stock item'),
'default': False,
},
{
'name': 'copy_history',
'label': _('Copy History'),
'help_text': _('Copy stock history from the original stock item'),
'default': False,
},
],
)
def validate_part(self, part):
"""Ensure the provided Part instance is valid."""
if part.virtual:

View File

@ -27,6 +27,7 @@ import { modals } from '@mantine/modals';
import {
IconCalendarExclamation,
IconCoins,
IconCopy,
IconCurrencyDollar,
IconLink,
IconPackage,
@ -81,7 +82,8 @@ export function useStockFields({
create = false,
supplierPartId,
pricing,
modalId
modalId,
duplicateStockItem
}: {
partId?: number;
stockItem?: any;
@ -89,6 +91,7 @@ export function useStockFields({
create: boolean;
supplierPartId?: number;
pricing?: { [priceBreak: number]: [number, string] };
duplicateStockItem?: any;
}): ApiFormFieldSet {
const globalSettings = useGlobalSettingsState();
@ -301,6 +304,25 @@ export function useStockFields({
delete fields.serial_numbers;
}
// Additional fields for stock item duplication
if (create && duplicateStockItem?.pk) {
fields.duplicate = {
icon: <IconCopy />,
children: {
original: {
value: duplicateStockItem.pk,
hidden: true
},
copy_notes: { value: true },
copy_history: { value: false },
copy_tests: {
value: false,
hidden: !duplicateStockItem?.part_detail?.testable
}
}
};
}
return fields;
}, [
stockItem,
@ -315,6 +337,7 @@ export function useStockFields({
purchasePriceCurrency,
serialGenerator.result,
batchGenerator.result,
duplicateStockItem,
create
]);
}

View File

@ -742,7 +742,8 @@ export default function StockDetail() {
const duplicateStockItemFields = useStockFields({
create: true,
modalId: 'duplicate-stock-item'
modalId: 'duplicate-stock-item',
duplicateStockItem: stockitem
});
const duplicateStockData = useMemo(() => {