diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index abeb1229c6..7b001c7b58 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -737,10 +737,12 @@ class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI): 'quantity', 'location', 'reference', + 'IPN', ] ordering_field_aliases = { 'part': 'stock_item__part__name', + 'IPN': 'stock_item__part__IPN', 'sku': 'stock_item__supplier_part__SKU', 'location': 'stock_item__location__name', 'reference': 'build_line__bom_item__reference', @@ -749,6 +751,7 @@ class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI): search_fields = [ 'stock_item__supplier_part__SKU', 'stock_item__part__name', + 'stock_item__part__IPN', 'build_line__bom_item__reference', ] diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index a851b7af91..4b72f69600 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -315,6 +315,10 @@ function TableAnchorValue(props: Readonly) { } function ProgressBarValue(props: Readonly) { + if (props.field_data.total <= 0) { + return {props.field_data.progress}; + } + return ( (null); + // Keep track of the "part" instance + const [partInstance, setPartInstance] = useState({}); + const [supplierPart, setSupplierPart] = useState(null); - const [batchCode, setBatchCode] = useState(''); - const [serialNumbers, setSerialNumbers] = useState(''); - - const [trackable, setTrackable] = useState(false); + const [nextBatchCode, setNextBatchCode] = useState(''); + const [nextSerialNumber, setNextSerialNumber] = useState(''); const batchGenerator = useBatchCodeGenerator((value: any) => { - if (!batchCode) { - setBatchCode(value); + if (value) { + setNextBatchCode(`Next batch code` + `: ${value}`); + } else { + setNextBatchCode(''); } }); const serialGenerator = useSerialNumberGenerator((value: any) => { - if (!serialNumbers && create && trackable) { - setSerialNumbers(value); + if (value) { + setNextSerialNumber(t`Next serial number` + `: ${value}`); + } else { + setNextSerialNumber(''); } }); + useEffect(() => { + if (partInstance?.pk) { + // Update the generators whenever the part ID changes + batchGenerator.update({ part: partInstance.pk }); + serialGenerator.update({ part: partInstance.pk }); + } + }, [partInstance.pk]); + return useMemo(() => { const fields: ApiFormFieldSet = { part: { - value: partId, + value: partInstance.pk, disabled: !create, filters: { active: create ? true : undefined }, onValueChange: (value, record) => { - setPart(value); - // TODO: implement remaining functionality from old stock.py - - setTrackable(record.trackable ?? false); - - batchGenerator.update({ part: value }); - serialGenerator.update({ part: value }); - - if (!record.trackable) { - setSerialNumbers(''); - } + // Update the tracked part instance + setPartInstance(record); // Clear the 'supplier_part' field if the part is changed setSupplierPart(null); } }, supplier_part: { - hidden: part_detail?.purchaseable == false, + hidden: partInstance?.purchaseable == false, value: supplierPart, onValueChange: (value) => { setSupplierPart(value); @@ -114,7 +116,7 @@ export function useStockFields({ filters: { part_detail: true, supplier_detail: true, - ...(part ? { part } : {}) + part: partId }, adjustFilters: (adjust: ApiFormAdjustFilterType) => { if (adjust.data.part) { @@ -148,22 +150,20 @@ export function useStockFields({ serial_numbers: { field_type: 'string', label: t`Serial Numbers`, + disabled: partInstance?.trackable == false, description: t`Enter serial numbers for new stock (or leave blank)`, required: false, - disabled: !trackable, hidden: !create, - value: serialNumbers, - onValueChange: (value) => setSerialNumbers(value) + placeholder: nextSerialNumber }, serial: { hidden: create || - part_detail?.trackable == false || - (!item_detail?.quantity != undefined && item_detail?.quantity != 1) + partInstance.trackable == false || + (!stockItem?.quantity != undefined && stockItem?.quantity != 1) }, batch: { - value: batchCode, - onValueChange: (value) => setBatchCode(value) + placeholder: nextBatchCode }, status_custom_key: { label: t`Stock Status` @@ -195,16 +195,14 @@ export function useStockFields({ return fields; }, [ - item_detail, - part_detail, - part, + stockItem, + partInstance, + partId, globalSettings, supplierPart, - batchCode, - serialNumbers, - trackable, - create, - partId + nextSerialNumber, + nextBatchCode, + create ]); } diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx index 32414e58eb..d564647696 100644 --- a/src/frontend/src/hooks/UseInstance.tsx +++ b/src/frontend/src/hooks/UseInstance.tsx @@ -42,7 +42,13 @@ export function useInstance({ const [requestStatus, setRequestStatus] = useState(0); const instanceQuery = useQuery({ - queryKey: ['instance', endpoint, pk, params, pathParams], + queryKey: [ + 'instance', + endpoint, + pk, + JSON.stringify(params), + JSON.stringify(pathParams) + ], queryFn: async () => { if (hasPrimaryKey) { if ( diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx index fc5c32f40d..e60fbea87b 100644 --- a/src/frontend/src/pages/company/SupplierPartDetail.tsx +++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx @@ -96,9 +96,10 @@ export default function SupplierPartDetail() { { type: 'string', name: 'part_detail.description', - label: t`Description`, + label: t`Part Description`, copy: true, - icon: 'info' + icon: 'info', + hidden: !data.part_detail?.description }, { type: 'link', @@ -133,6 +134,13 @@ export default function SupplierPartDetail() { copy: true, icon: 'reference' }, + { + type: 'string', + name: 'description', + label: t`Description`, + copy: true, + hidden: !data.description + }, { type: 'link', name: 'manufacturer', diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 2d6d4ecef4..78bf9565e5 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -118,6 +118,14 @@ export default function PartDetail() { const globalSettings = useGlobalSettingsState(); const userSettings = useUserSettingsState(); + const { instance: serials } = useInstance({ + endpoint: ApiEndpoints.part_serial_numbers, + pk: id, + hasPrimaryKey: true, + refetchOnMount: false, + defaultValue: {} + }); + const { instance: part, refreshInstance, @@ -132,15 +140,22 @@ export default function PartDetail() { refetchOnMount: true }); - part.required = - (part?.required_for_build_orders ?? 0) + - (part?.required_for_sales_orders ?? 0); - const detailsPanel = useMemo(() => { if (instanceQuery.isFetching) { return ; } + let data = { ...part }; + + data.required = + (data?.required_for_build_orders ?? 0) + + (data?.required_for_sales_orders ?? 0); + + // Provide latest serial number info + if (!!serials.latest) { + data.latest_serial_number = serials.latest; + } + // Construct the details tables let tl: DetailsField[] = [ { @@ -277,7 +292,10 @@ export default function PartDetail() { total: part.required_for_build_orders, progress: part.allocated_to_build_orders, label: t`Allocated to Build Orders`, - hidden: !part.component || part.required_for_build_orders <= 0 + hidden: + !part.component || + (part.required_for_build_orders <= 0 && + part.allocated_to_build_orders <= 0) }, { type: 'progressbar', @@ -285,7 +303,10 @@ export default function PartDetail() { total: part.required_for_sales_orders, progress: part.allocated_to_sales_orders, label: t`Allocated to Sales Orders`, - hidden: !part.salable || part.required_for_sales_orders <= 0 + hidden: + !part.salable || + (part.required_for_sales_orders <= 0 && + part.allocated_to_sales_orders <= 0) }, { type: 'string', @@ -349,6 +370,7 @@ export default function PartDetail() { { type: 'boolean', name: 'salable', + icon: 'saleable', label: t`Saleable Part` }, { @@ -434,6 +456,14 @@ export default function PartDetail() { }); } + br.push({ + type: 'string', + name: 'latest_serial_number', + label: t`Latest Serial Number`, + hidden: !part.trackable || !data.latest_serial_number, + icon: 'serial' + }); + // Add in stocktake information if (id && part.last_stocktake) { br.push({ @@ -526,17 +556,17 @@ export default function PartDetail() { /> - + - - - + + + ) : ( ); - }, [globalSettings, part, instanceQuery]); + }, [globalSettings, part, serials, instanceQuery]); // Part data panels (recalculate when part data changes) const partPanels: PanelType[] = useMemo(() => { diff --git a/src/frontend/src/pages/part/PartSchedulingDetail.tsx b/src/frontend/src/pages/part/PartSchedulingDetail.tsx index 2e5411f099..612ef028cc 100644 --- a/src/frontend/src/pages/part/PartSchedulingDetail.tsx +++ b/src/frontend/src/pages/part/PartSchedulingDetail.tsx @@ -1,7 +1,7 @@ import { t } from '@lingui/macro'; import { ChartTooltipProps, LineChart } from '@mantine/charts'; import { - Anchor, + Alert, Center, Divider, Loader, @@ -65,23 +65,7 @@ export default function PartSchedulingDetail({ part }: { part: any }) { { accessor: 'label', switchable: false, - title: t`Order`, - render: (record: any) => { - const url = getDetailUrl(record.model, record.model_id); - - if (url) { - return ( - navigateToLink(url, navigate, event)} - > - {record.label} - - ); - } else { - return record.label; - } - } + title: t`Order` }, DescriptionColumn({ accessor: 'title', @@ -245,15 +229,32 @@ export default function PartSchedulingDetail({ part }: { part: any }) { return [min_date.valueOf(), max_date.valueOf()]; }, [chartData]); + const hasSchedulingInfo: boolean = useMemo( + () => table.recordCount > 0, + [table.recordCount] + ); + return ( <> + {!table.isLoading && !hasSchedulingInfo && ( + + {t`There is no scheduling information available for the selected part`} + + )} { + const url = getDetailUrl(record.model, record.model_id); + + if (url) { + navigateToLink(url, navigate, event); + } + } }} /> {table.isLoading ? ( diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index dbbd3eabd2..7c9a622373 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -525,7 +525,7 @@ export default function StockDetail() { const editStockItemFields = useStockFields({ create: false, - part_detail: stockitem.part_detail + partId: stockitem.part }); const editStockItem = useEditApiFormModal({ diff --git a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx index 5eccbf9b76..379afd242e 100644 --- a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx +++ b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx @@ -97,6 +97,14 @@ export default function BuildAllocatedStockTable({ switchable: false, render: (record: any) => PartColumn({ part: record.part_detail }) }, + { + accessor: 'part_detail.IPN', + ordering: 'IPN', + hidden: !showPartInfo, + title: t`IPN`, + sortable: true, + switchable: true + }, { hidden: !showPartInfo, accessor: 'bom_reference', diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index 7bf62a44f6..fbea651f5d 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -218,8 +218,8 @@ export default function BuildOutputTable({ const editStockItemFields = useStockFields({ create: false, - item_detail: selectedOutputs[0], - part_detail: selectedOutputs[0]?.part_detail + partId: partId, + stockItem: selectedOutputs[0] }); const editBuildOutput = useEditApiFormModal({ diff --git a/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx b/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx index 6c2057966e..643d5fd570 100644 --- a/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx +++ b/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx @@ -95,10 +95,6 @@ export default function PartPurchaseOrdersTable({ ); } }, - DateColumn({ - accessor: 'order_detail.complete_date', - title: t`Order Completed Date` - }), DateColumn({ accessor: 'target_date', title: t`Target Date` diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx index 2f745cba4e..9677a17156 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -149,7 +149,10 @@ export function PurchaseOrderLineItemTable({ let part = record?.part_detail ?? supplier_part?.part_detail ?? {}; let extra = []; - if (supplier_part.pack_quantity_native != 1) { + if ( + supplier_part?.pack_quantity_native != undefined && + supplier_part.pack_quantity_native != 1 + ) { let total = record.quantity * supplier_part.pack_quantity_native; extra.push(