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',
'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',
]

View File

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

View File

@ -40,6 +40,7 @@ import {
useBatchCodeGenerator,
useSerialNumberGenerator
} from '../hooks/UseGenerator';
import { useInstance } from '../hooks/UseInstance';
import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder';
import { apiUrl } from '../states/ApiState';
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
*/
export function useStockFields({
item_detail,
part_detail,
partId,
stockItem,
create = false
}: {
partId?: number;
item_detail?: any;
part_detail?: any;
stockItem?: any;
create: boolean;
}): ApiFormFieldSet {
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 [batchCode, setBatchCode] = useState<string>('');
const [serialNumbers, setSerialNumbers] = useState<string>('');
const [trackable, setTrackable] = useState<boolean>(false);
const [nextBatchCode, setNextBatchCode] = useState<string>('');
const [nextSerialNumber, setNextSerialNumber] = useState<string>('');
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
]);
}

View File

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

View File

@ -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',

View File

@ -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 <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
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() {
/>
</Grid.Col>
<Grid.Col span={8}>
<DetailsTable fields={tl} item={part} />
<DetailsTable fields={tl} item={data} />
</Grid.Col>
</Grid>
<DetailsTable fields={tr} item={part} />
<DetailsTable fields={bl} item={part} />
<DetailsTable fields={br} item={part} />
<DetailsTable fields={tr} item={data} />
<DetailsTable fields={bl} item={data} />
<DetailsTable fields={br} item={data} />
</ItemDetailsGrid>
) : (
<Skeleton />
);
}, [globalSettings, part, instanceQuery]);
}, [globalSettings, part, serials, instanceQuery]);
// Part data panels (recalculate when part data changes)
const partPanels: PanelType[] = useMemo(() => {

View File

@ -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 (
<Anchor
href="#"
onClick={(event: any) => navigateToLink(url, navigate, event)}
>
{record.label}
</Anchor>
);
} 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 && (
<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}>
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_scheduling, part.pk)}
tableState={table}
columns={tableColumns}
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 ? (

View File

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

View File

@ -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',

View File

@ -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({

View File

@ -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`

View File

@ -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(