2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

[PUI] Platform fixes (#8324)

* Add "IPN" column to build order allocated stock table

* Allow sorting and searching by IPN

* Handle allocations where allocated but required == 0

* Add "no info available" message to part scheduling

* Adjust PartSchedulingTable

* Icon fix

* Add "latest serial number" information to PartDetail page

* Cleanup code for serial-number placeholder in forms

* Logic fix for displaying non-unity pack quantity

* Fix description field on SupplierPart page

* Fix duplicate table column
This commit is contained in:
Oliver 2024-10-22 12:17:24 +11:00 committed by GitHub
parent e219b7c914
commit ddea9fa4b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 140 additions and 83 deletions

View File

@ -737,10 +737,12 @@ class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
'quantity', 'quantity',
'location', 'location',
'reference', 'reference',
'IPN',
] ]
ordering_field_aliases = { ordering_field_aliases = {
'part': 'stock_item__part__name', 'part': 'stock_item__part__name',
'IPN': 'stock_item__part__IPN',
'sku': 'stock_item__supplier_part__SKU', 'sku': 'stock_item__supplier_part__SKU',
'location': 'stock_item__location__name', 'location': 'stock_item__location__name',
'reference': 'build_line__bom_item__reference', 'reference': 'build_line__bom_item__reference',
@ -749,6 +751,7 @@ class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
search_fields = [ search_fields = [
'stock_item__supplier_part__SKU', 'stock_item__supplier_part__SKU',
'stock_item__part__name', 'stock_item__part__name',
'stock_item__part__IPN',
'build_line__bom_item__reference', 'build_line__bom_item__reference',
] ]

View File

@ -315,6 +315,10 @@ function TableAnchorValue(props: Readonly<FieldProps>) {
} }
function ProgressBarValue(props: Readonly<FieldProps>) { function ProgressBarValue(props: Readonly<FieldProps>) {
if (props.field_data.total <= 0) {
return <Text size="sm">{props.field_data.progress}</Text>;
}
return ( return (
<ProgressBar <ProgressBar
value={props.field_data.progress} value={props.field_data.progress}

View File

@ -40,6 +40,7 @@ import {
useBatchCodeGenerator, useBatchCodeGenerator,
useSerialNumberGenerator useSerialNumberGenerator
} from '../hooks/UseGenerator'; } from '../hooks/UseGenerator';
import { useInstance } from '../hooks/UseInstance';
import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder'; import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder';
import { apiUrl } from '../states/ApiState'; import { apiUrl } from '../states/ApiState';
import { useGlobalSettingsState } from '../states/SettingsState'; import { useGlobalSettingsState } from '../states/SettingsState';
@ -48,65 +49,66 @@ import { useGlobalSettingsState } from '../states/SettingsState';
* Construct a set of fields for creating / editing a StockItem instance * Construct a set of fields for creating / editing a StockItem instance
*/ */
export function useStockFields({ export function useStockFields({
item_detail,
part_detail,
partId, partId,
stockItem,
create = false create = false
}: { }: {
partId?: number; partId?: number;
item_detail?: any; stockItem?: any;
part_detail?: any;
create: boolean; create: boolean;
}): ApiFormFieldSet { }): ApiFormFieldSet {
const globalSettings = useGlobalSettingsState(); const globalSettings = useGlobalSettingsState();
const [part, setPart] = useState<number | null>(null); // Keep track of the "part" instance
const [partInstance, setPartInstance] = useState<any>({});
const [supplierPart, setSupplierPart] = useState<number | null>(null); const [supplierPart, setSupplierPart] = useState<number | null>(null);
const [batchCode, setBatchCode] = useState<string>(''); const [nextBatchCode, setNextBatchCode] = useState<string>('');
const [serialNumbers, setSerialNumbers] = useState<string>(''); const [nextSerialNumber, setNextSerialNumber] = useState<string>('');
const [trackable, setTrackable] = useState<boolean>(false);
const batchGenerator = useBatchCodeGenerator((value: any) => { const batchGenerator = useBatchCodeGenerator((value: any) => {
if (!batchCode) { if (value) {
setBatchCode(value); setNextBatchCode(`Next batch code` + `: ${value}`);
} else {
setNextBatchCode('');
} }
}); });
const serialGenerator = useSerialNumberGenerator((value: any) => { const serialGenerator = useSerialNumberGenerator((value: any) => {
if (!serialNumbers && create && trackable) { if (value) {
setSerialNumbers(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(() => { return useMemo(() => {
const fields: ApiFormFieldSet = { const fields: ApiFormFieldSet = {
part: { part: {
value: partId, value: partInstance.pk,
disabled: !create, disabled: !create,
filters: { filters: {
active: create ? true : undefined active: create ? true : undefined
}, },
onValueChange: (value, record) => { onValueChange: (value, record) => {
setPart(value); // Update the tracked part instance
// TODO: implement remaining functionality from old stock.py setPartInstance(record);
setTrackable(record.trackable ?? false);
batchGenerator.update({ part: value });
serialGenerator.update({ part: value });
if (!record.trackable) {
setSerialNumbers('');
}
// Clear the 'supplier_part' field if the part is changed // Clear the 'supplier_part' field if the part is changed
setSupplierPart(null); setSupplierPart(null);
} }
}, },
supplier_part: { supplier_part: {
hidden: part_detail?.purchaseable == false, hidden: partInstance?.purchaseable == false,
value: supplierPart, value: supplierPart,
onValueChange: (value) => { onValueChange: (value) => {
setSupplierPart(value); setSupplierPart(value);
@ -114,7 +116,7 @@ export function useStockFields({
filters: { filters: {
part_detail: true, part_detail: true,
supplier_detail: true, supplier_detail: true,
...(part ? { part } : {}) part: partId
}, },
adjustFilters: (adjust: ApiFormAdjustFilterType) => { adjustFilters: (adjust: ApiFormAdjustFilterType) => {
if (adjust.data.part) { if (adjust.data.part) {
@ -148,22 +150,20 @@ export function useStockFields({
serial_numbers: { serial_numbers: {
field_type: 'string', field_type: 'string',
label: t`Serial Numbers`, label: t`Serial Numbers`,
disabled: partInstance?.trackable == false,
description: t`Enter serial numbers for new stock (or leave blank)`, description: t`Enter serial numbers for new stock (or leave blank)`,
required: false, required: false,
disabled: !trackable,
hidden: !create, hidden: !create,
value: serialNumbers, placeholder: nextSerialNumber
onValueChange: (value) => setSerialNumbers(value)
}, },
serial: { serial: {
hidden: hidden:
create || create ||
part_detail?.trackable == false || partInstance.trackable == false ||
(!item_detail?.quantity != undefined && item_detail?.quantity != 1) (!stockItem?.quantity != undefined && stockItem?.quantity != 1)
}, },
batch: { batch: {
value: batchCode, placeholder: nextBatchCode
onValueChange: (value) => setBatchCode(value)
}, },
status_custom_key: { status_custom_key: {
label: t`Stock Status` label: t`Stock Status`
@ -195,16 +195,14 @@ export function useStockFields({
return fields; return fields;
}, [ }, [
item_detail, stockItem,
part_detail, partInstance,
part, partId,
globalSettings, globalSettings,
supplierPart, supplierPart,
batchCode, nextSerialNumber,
serialNumbers, nextBatchCode,
trackable, create
create,
partId
]); ]);
} }

View File

@ -42,7 +42,13 @@ export function useInstance<T = any>({
const [requestStatus, setRequestStatus] = useState<number>(0); const [requestStatus, setRequestStatus] = useState<number>(0);
const instanceQuery = useQuery<T>({ const instanceQuery = useQuery<T>({
queryKey: ['instance', endpoint, pk, params, pathParams], queryKey: [
'instance',
endpoint,
pk,
JSON.stringify(params),
JSON.stringify(pathParams)
],
queryFn: async () => { queryFn: async () => {
if (hasPrimaryKey) { if (hasPrimaryKey) {
if ( if (

View File

@ -96,9 +96,10 @@ export default function SupplierPartDetail() {
{ {
type: 'string', type: 'string',
name: 'part_detail.description', name: 'part_detail.description',
label: t`Description`, label: t`Part Description`,
copy: true, copy: true,
icon: 'info' icon: 'info',
hidden: !data.part_detail?.description
}, },
{ {
type: 'link', type: 'link',
@ -133,6 +134,13 @@ export default function SupplierPartDetail() {
copy: true, copy: true,
icon: 'reference' icon: 'reference'
}, },
{
type: 'string',
name: 'description',
label: t`Description`,
copy: true,
hidden: !data.description
},
{ {
type: 'link', type: 'link',
name: 'manufacturer', name: 'manufacturer',

View File

@ -118,6 +118,14 @@ export default function PartDetail() {
const globalSettings = useGlobalSettingsState(); const globalSettings = useGlobalSettingsState();
const userSettings = useUserSettingsState(); const userSettings = useUserSettingsState();
const { instance: serials } = useInstance({
endpoint: ApiEndpoints.part_serial_numbers,
pk: id,
hasPrimaryKey: true,
refetchOnMount: false,
defaultValue: {}
});
const { const {
instance: part, instance: part,
refreshInstance, refreshInstance,
@ -132,15 +140,22 @@ export default function PartDetail() {
refetchOnMount: true refetchOnMount: true
}); });
part.required =
(part?.required_for_build_orders ?? 0) +
(part?.required_for_sales_orders ?? 0);
const detailsPanel = useMemo(() => { const detailsPanel = useMemo(() => {
if (instanceQuery.isFetching) { if (instanceQuery.isFetching) {
return <Skeleton />; return <Skeleton />;
} }
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 // Construct the details tables
let tl: DetailsField[] = [ let tl: DetailsField[] = [
{ {
@ -277,7 +292,10 @@ export default function PartDetail() {
total: part.required_for_build_orders, total: part.required_for_build_orders,
progress: part.allocated_to_build_orders, progress: part.allocated_to_build_orders,
label: t`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', type: 'progressbar',
@ -285,7 +303,10 @@ export default function PartDetail() {
total: part.required_for_sales_orders, total: part.required_for_sales_orders,
progress: part.allocated_to_sales_orders, progress: part.allocated_to_sales_orders,
label: t`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', type: 'string',
@ -349,6 +370,7 @@ export default function PartDetail() {
{ {
type: 'boolean', type: 'boolean',
name: 'salable', name: 'salable',
icon: 'saleable',
label: t`Saleable Part` 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 // Add in stocktake information
if (id && part.last_stocktake) { if (id && part.last_stocktake) {
br.push({ br.push({
@ -526,17 +556,17 @@ export default function PartDetail() {
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={8}> <Grid.Col span={8}>
<DetailsTable fields={tl} item={part} /> <DetailsTable fields={tl} item={data} />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
<DetailsTable fields={tr} item={part} /> <DetailsTable fields={tr} item={data} />
<DetailsTable fields={bl} item={part} /> <DetailsTable fields={bl} item={data} />
<DetailsTable fields={br} item={part} /> <DetailsTable fields={br} item={data} />
</ItemDetailsGrid> </ItemDetailsGrid>
) : ( ) : (
<Skeleton /> <Skeleton />
); );
}, [globalSettings, part, instanceQuery]); }, [globalSettings, part, serials, instanceQuery]);
// Part data panels (recalculate when part data changes) // Part data panels (recalculate when part data changes)
const partPanels: PanelType[] = useMemo(() => { const partPanels: PanelType[] = useMemo(() => {

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ChartTooltipProps, LineChart } from '@mantine/charts'; import { ChartTooltipProps, LineChart } from '@mantine/charts';
import { import {
Anchor, Alert,
Center, Center,
Divider, Divider,
Loader, Loader,
@ -65,23 +65,7 @@ export default function PartSchedulingDetail({ part }: { part: any }) {
{ {
accessor: 'label', accessor: 'label',
switchable: false, switchable: false,
title: t`Order`, title: t`Order`
render: (record: any) => {
const url = getDetailUrl(record.model, record.model_id);
if (url) {
return (
<Anchor
href="#"
onClick={(event: any) => navigateToLink(url, navigate, event)}
>
{record.label}
</Anchor>
);
} else {
return record.label;
}
}
}, },
DescriptionColumn({ DescriptionColumn({
accessor: 'title', accessor: 'title',
@ -245,15 +229,32 @@ export default function PartSchedulingDetail({ part }: { part: any }) {
return [min_date.valueOf(), max_date.valueOf()]; return [min_date.valueOf(), max_date.valueOf()];
}, [chartData]); }, [chartData]);
const hasSchedulingInfo: boolean = useMemo(
() => table.recordCount > 0,
[table.recordCount]
);
return ( return (
<> <>
{!table.isLoading && !hasSchedulingInfo && (
<Alert color="blue" title={t`No information available`}>
<Text>{t`There is no scheduling information available for the selected part`}</Text>
</Alert>
)}
<SimpleGrid cols={2}> <SimpleGrid cols={2}>
<InvenTreeTable <InvenTreeTable
url={apiUrl(ApiEndpoints.part_scheduling, part.pk)} url={apiUrl(ApiEndpoints.part_scheduling, part.pk)}
tableState={table} tableState={table}
columns={tableColumns} columns={tableColumns}
props={{ props={{
enableSearch: false enableSearch: false,
onRowClick: (record: any, index: number, event: any) => {
const url = getDetailUrl(record.model, record.model_id);
if (url) {
navigateToLink(url, navigate, event);
}
}
}} }}
/> />
{table.isLoading ? ( {table.isLoading ? (

View File

@ -525,7 +525,7 @@ export default function StockDetail() {
const editStockItemFields = useStockFields({ const editStockItemFields = useStockFields({
create: false, create: false,
part_detail: stockitem.part_detail partId: stockitem.part
}); });
const editStockItem = useEditApiFormModal({ const editStockItem = useEditApiFormModal({

View File

@ -97,6 +97,14 @@ export default function BuildAllocatedStockTable({
switchable: false, switchable: false,
render: (record: any) => PartColumn({ part: record.part_detail }) 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, hidden: !showPartInfo,
accessor: 'bom_reference', accessor: 'bom_reference',

View File

@ -218,8 +218,8 @@ export default function BuildOutputTable({
const editStockItemFields = useStockFields({ const editStockItemFields = useStockFields({
create: false, create: false,
item_detail: selectedOutputs[0], partId: partId,
part_detail: selectedOutputs[0]?.part_detail stockItem: selectedOutputs[0]
}); });
const editBuildOutput = useEditApiFormModal({ const editBuildOutput = useEditApiFormModal({

View File

@ -95,10 +95,6 @@ export default function PartPurchaseOrdersTable({
); );
} }
}, },
DateColumn({
accessor: 'order_detail.complete_date',
title: t`Order Completed Date`
}),
DateColumn({ DateColumn({
accessor: 'target_date', accessor: 'target_date',
title: t`Target Date` title: t`Target Date`

View File

@ -149,7 +149,10 @@ export function PurchaseOrderLineItemTable({
let part = record?.part_detail ?? supplier_part?.part_detail ?? {}; let part = record?.part_detail ?? supplier_part?.part_detail ?? {};
let extra = []; 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; let total = record.quantity * supplier_part.pack_quantity_native;
extra.push( extra.push(