mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 19:46:46 +00:00
[PUI] Serialize Stock (#8192)
* Add PUI form to serialize existing stock item * Remove debug statement * Ensure that stock item trees are rebuilt correctly after serialization - No idea how this has not been detected previously * Add unit test to ensure child_items annotation works as expected * Add link to parent item in stock detail page * Refactor to use new placeholder hook
This commit is contained in:
parent
23de1e038d
commit
a5f2273e14
@ -1572,6 +1572,13 @@ class StockItem(
|
||||
# Remove the equivalent number of items
|
||||
self.take_stock(quantity, user, notes=notes)
|
||||
|
||||
# Rebuild the stock tree
|
||||
try:
|
||||
StockItem.objects.partial_rebuild(tree_id=self.tree_id)
|
||||
except Exception:
|
||||
logger.warning('Failed to rebuild stock tree during serializeStock')
|
||||
StockItem.objects.rebuild()
|
||||
|
||||
@transaction.atomic
|
||||
def copyHistoryFrom(self, other):
|
||||
"""Copy stock history from another StockItem."""
|
||||
@ -1806,7 +1813,7 @@ class StockItem(
|
||||
for tree_id in tree_ids:
|
||||
StockItem.objects.partial_rebuild(tree_id=tree_id)
|
||||
except Exception:
|
||||
logger.warning('Rebuilding entire StockItem tree')
|
||||
logger.warning('Rebuilding entire StockItem tree during merge_stock_items')
|
||||
StockItem.objects.rebuild()
|
||||
|
||||
@transaction.atomic
|
||||
|
@ -925,6 +925,46 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.list_url, {'location_detail': True, 'tests': True}, max_query_count=35
|
||||
)
|
||||
|
||||
def test_child_items(self):
|
||||
"""Test that the 'child_items' annotation works as expected."""
|
||||
# Create a trackable part
|
||||
my_part = Part.objects.create(
|
||||
name='Test Part', description='Test Part Description', trackable=True
|
||||
)
|
||||
|
||||
# Create an initial stock item
|
||||
parent_item = StockItem.objects.create(
|
||||
part=my_part, quantity=10, location=StockLocation.objects.first()
|
||||
)
|
||||
|
||||
# Serialize this stock item
|
||||
parent_item.serializeStock(
|
||||
5, [1, 2, 3, 4, 5], user=self.user, notes='Some notes'
|
||||
)
|
||||
|
||||
parent_item.refresh_from_db()
|
||||
|
||||
# Check that the parent item has 5 child items
|
||||
self.assertEqual(parent_item.get_descendants(include_self=False).count(), 5)
|
||||
self.assertEqual(my_part.stock_items.count(), 6)
|
||||
|
||||
# Fetch stock list via API
|
||||
response = self.get(reverse('api-stock-list'), {'part': my_part.pk})
|
||||
|
||||
self.assertEqual(len(response.data), 6)
|
||||
|
||||
# Fetch stock detail
|
||||
response = self.get(reverse('api-stock-detail', kwargs={'pk': parent_item.pk}))
|
||||
|
||||
self.assertEqual(response.data['child_items'], 5)
|
||||
|
||||
for child in parent_item.get_children():
|
||||
response = self.get(reverse('api-stock-detail', kwargs={'pk': child.pk}))
|
||||
|
||||
self.assertEqual(response.data['parent'], parent_item.pk)
|
||||
self.assertEqual(response.data['quantity'], 1)
|
||||
self.assertEqual(response.data['child_items'], 0)
|
||||
|
||||
|
||||
class CustomStockItemStatusTest(StockAPITestCase):
|
||||
"""Tests for custom stock item statuses."""
|
||||
|
@ -116,12 +116,14 @@ export enum ApiEndpoints {
|
||||
manufacturer_part_list = 'company/part/manufacturer/',
|
||||
manufacturer_part_parameter_list = 'company/part/manufacturer/parameter/',
|
||||
|
||||
// Stock API endpoints
|
||||
stock_item_list = 'stock/',
|
||||
stock_tracking_list = 'stock/track/',
|
||||
// Stock location endpoints
|
||||
stock_location_list = 'stock/location/',
|
||||
stock_location_type_list = 'stock/location-type/',
|
||||
stock_location_tree = 'stock/location/tree/',
|
||||
|
||||
// Stock item API endpoints
|
||||
stock_item_list = 'stock/',
|
||||
stock_tracking_list = 'stock/track/',
|
||||
stock_test_result_list = 'stock/test/',
|
||||
stock_transfer = 'stock/transfer/',
|
||||
stock_remove = 'stock/remove/',
|
||||
@ -131,9 +133,10 @@ export enum ApiEndpoints {
|
||||
stock_merge = 'stock/merge/',
|
||||
stock_assign = 'stock/assign/',
|
||||
stock_status = 'stock/status/',
|
||||
stock_install = 'stock/:id/install',
|
||||
build_test_statistics = 'test-statistics/by-build/:id',
|
||||
part_test_statistics = 'test-statistics/by-part/:id',
|
||||
stock_install = 'stock/:id/install/',
|
||||
stock_serialize = 'stock/:id/serialize/',
|
||||
build_test_statistics = 'test-statistics/by-build/:id/',
|
||||
part_test_statistics = 'test-statistics/by-part/:id/',
|
||||
|
||||
// Generator API endpoints
|
||||
generate_batch_code = 'generate/batch-code/',
|
||||
|
@ -40,6 +40,7 @@ import {
|
||||
useBatchCodeGenerator,
|
||||
useSerialNumberGenerator
|
||||
} from '../hooks/UseGenerator';
|
||||
import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder';
|
||||
import { apiUrl } from '../states/ApiState';
|
||||
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||
|
||||
@ -214,6 +215,30 @@ export function useCreateStockItem() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useStockItemSerializeFields({
|
||||
partId,
|
||||
trackable
|
||||
}: {
|
||||
partId: number;
|
||||
trackable: boolean;
|
||||
}) {
|
||||
const snPlaceholder = useSerialNumberPlaceholder({
|
||||
partId: partId,
|
||||
key: 'stock-item-serialize',
|
||||
enabled: trackable
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
quantity: {},
|
||||
serial_numbers: {
|
||||
placeholder: snPlaceholder
|
||||
},
|
||||
destination: {}
|
||||
};
|
||||
}, [snPlaceholder]);
|
||||
}
|
||||
|
||||
function StockItemDefaultMove({
|
||||
stockItem,
|
||||
value
|
||||
|
@ -90,7 +90,7 @@ export function useInstance<T = any>({
|
||||
});
|
||||
|
||||
const refreshInstance = useCallback(function () {
|
||||
instanceQuery.refetch();
|
||||
return instanceQuery.refetch();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
@ -39,12 +39,14 @@ import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
import { partCategoryFields } from '../../forms/PartForms';
|
||||
import {
|
||||
StockOperationProps,
|
||||
useAddStockItem,
|
||||
useCountStockItem,
|
||||
useRemoveStockItem,
|
||||
useStockFields,
|
||||
useStockItemSerializeFields,
|
||||
useTransferStockItem
|
||||
} from '../../forms/StockForms';
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
@ -203,6 +205,16 @@ export default function StockDetail() {
|
||||
model: ModelType.stockitem,
|
||||
hidden: !stockitem.belongs_to
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent',
|
||||
label: t`Parent Item`,
|
||||
model: ModelType.stockitem,
|
||||
hidden: !stockitem.parent,
|
||||
model_formatter: (model: any) => {
|
||||
return t`Parent stock item`;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'consumed_by',
|
||||
@ -481,8 +493,39 @@ export default function StockDetail() {
|
||||
const removeStockItem = useRemoveStockItem(stockActionProps);
|
||||
const transferStockItem = useTransferStockItem(stockActionProps);
|
||||
|
||||
const stockActions = useMemo(
|
||||
() => [
|
||||
const serializeStockFields = useStockItemSerializeFields({
|
||||
partId: stockitem.part,
|
||||
trackable: stockitem.part_detail?.trackable
|
||||
});
|
||||
|
||||
const serializeStockItem = useCreateApiFormModal({
|
||||
url: ApiEndpoints.stock_serialize,
|
||||
pk: stockitem.pk,
|
||||
title: t`Serialize Stock Item`,
|
||||
fields: serializeStockFields,
|
||||
initialData: {
|
||||
quantity: stockitem.quantity,
|
||||
destination: stockitem.location ?? stockitem.part_detail?.default_location
|
||||
},
|
||||
onFormSuccess: () => {
|
||||
const partId = stockitem.part;
|
||||
refreshInstance().catch(() => {
|
||||
// Part may have been deleted - redirect to the part detail page
|
||||
navigate(getDetailUrl(ModelType.part, partId));
|
||||
});
|
||||
},
|
||||
successMessage: t`Stock item serialized`
|
||||
});
|
||||
|
||||
const stockActions = useMemo(() => {
|
||||
const serial = stockitem.serial;
|
||||
const serialized =
|
||||
serial != null &&
|
||||
serial != undefined &&
|
||||
serial != '' &&
|
||||
stockitem.quantity == 1;
|
||||
|
||||
return [
|
||||
<AdminButton model={ModelType.stockitem} pk={stockitem.pk} />,
|
||||
<BarcodeActionDropdown
|
||||
model={ModelType.stockitem}
|
||||
@ -503,6 +546,7 @@ export default function StockDetail() {
|
||||
{
|
||||
name: t`Count`,
|
||||
tooltip: t`Count stock`,
|
||||
hidden: serialized,
|
||||
icon: (
|
||||
<InvenTreeIcon icon="stocktake" iconProps={{ color: 'blue' }} />
|
||||
),
|
||||
@ -513,6 +557,7 @@ export default function StockDetail() {
|
||||
{
|
||||
name: t`Add`,
|
||||
tooltip: t`Add stock`,
|
||||
hidden: serialized,
|
||||
icon: <InvenTreeIcon icon="add" iconProps={{ color: 'green' }} />,
|
||||
onClick: () => {
|
||||
stockitem.pk && addStockItem.open();
|
||||
@ -521,11 +566,21 @@ export default function StockDetail() {
|
||||
{
|
||||
name: t`Remove`,
|
||||
tooltip: t`Remove stock`,
|
||||
hidden: serialized,
|
||||
icon: <InvenTreeIcon icon="remove" iconProps={{ color: 'red' }} />,
|
||||
onClick: () => {
|
||||
stockitem.pk && removeStockItem.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Serialize`,
|
||||
tooltip: t`Serialize stock`,
|
||||
hidden: serialized || stockitem?.part_detail?.trackable != true,
|
||||
icon: <InvenTreeIcon icon="serial" iconProps={{ color: 'blue' }} />,
|
||||
onClick: () => {
|
||||
serializeStockItem.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Transfer`,
|
||||
tooltip: t`Transfer stock`,
|
||||
@ -555,9 +610,8 @@ export default function StockDetail() {
|
||||
})
|
||||
]}
|
||||
/>
|
||||
],
|
||||
[id, stockitem, user]
|
||||
);
|
||||
];
|
||||
}, [id, stockitem, user]);
|
||||
|
||||
const stockBadges: ReactNode[] = useMemo(() => {
|
||||
let available = (stockitem?.quantity ?? 0) - (stockitem?.allocated ?? 0);
|
||||
@ -642,6 +696,7 @@ export default function StockDetail() {
|
||||
{addStockItem.modal}
|
||||
{removeStockItem.modal}
|
||||
{transferStockItem.modal}
|
||||
{serializeStockItem.modal}
|
||||
</Stack>
|
||||
</InstanceDetail>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user