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:
parent
e219b7c914
commit
ddea9fa4b9
@ -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',
|
||||
]
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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',
|
||||
|
@ -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(() => {
|
||||
|
@ -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 ? (
|
||||
|
@ -525,7 +525,7 @@ export default function StockDetail() {
|
||||
|
||||
const editStockItemFields = useStockFields({
|
||||
create: false,
|
||||
part_detail: stockitem.part_detail
|
||||
partId: stockitem.part
|
||||
});
|
||||
|
||||
const editStockItem = useEditApiFormModal({
|
||||
|
@ -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',
|
||||
|
@ -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({
|
||||
|
@ -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`
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user