2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-18 18:28:18 +00:00

Order parts form (#10729)

* Improved SupplierPart rendering

* Adjust tooltips

* Remove debug msg

* Add component for loading and displaying part requirements

* Improved rendering

* Better icons
This commit is contained in:
Oliver
2025-11-01 10:14:53 +11:00
committed by GitHub
parent 442a616432
commit 5ea39936b8
4 changed files with 178 additions and 24 deletions

View File

@@ -1,21 +1,24 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { ActionButton } from '@lib/components/ActionButton'; import { ActionButton } from '@lib/components/ActionButton';
import type { FloatingPosition } from '@mantine/core';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
export default function RemoveRowButton({ export default function RemoveRowButton({
onClick, onClick,
tooltip = t`Remove this row` tooltip = t`Remove this row`,
tooltipAlignment
}: Readonly<{ }: Readonly<{
onClick: () => void; onClick: () => void;
tooltip?: string; tooltip?: string;
tooltipAlignment?: FloatingPosition;
}>) { }>) {
return ( return (
<ActionButton <ActionButton
onClick={onClick} onClick={onClick}
icon={<InvenTreeIcon icon='square_x' />} icon={<InvenTreeIcon icon='square_x' />}
tooltip={tooltip} tooltip={tooltip}
tooltipAlignment='top-end' tooltipAlignment={tooltipAlignment ?? 'top-end'}
color='red' color='red'
/> />
); );

View File

@@ -80,7 +80,7 @@ export function RenderSupplierPart(
const part = instance.part_detail ?? {}; const part = instance.part_detail ?? {};
const secondary: string = instance.SKU; const secondary: string = instance.SKU;
let suffix: string = part.full_name; let suffix: string = part?.full_name ?? '';
if (instance.pack_quantity) { if (instance.pack_quantity) {
suffix += ` (${instance.pack_quantity})`; suffix += ` (${instance.pack_quantity})`;

View File

@@ -3,22 +3,163 @@ import { AddItemButton } from '@lib/components/AddItemButton';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api'; import { apiUrl } from '@lib/functions/Api';
import { formatDecimal } from '@lib/functions/Formatting';
import type { ApiFormFieldSet } from '@lib/types/Forms'; import type { ApiFormFieldSet } from '@lib/types/Forms';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { Alert, Group, Paper, Tooltip } from '@mantine/core'; import {
ActionIcon,
Alert,
Divider,
Group,
HoverCard,
Loader,
Paper,
Stack,
Text,
Tooltip
} from '@mantine/core';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { IconShoppingCart } from '@tabler/icons-react'; import {
IconExclamationCircle,
IconInfoCircle,
IconShoppingCart
} from '@tabler/icons-react';
import { DataTable } from 'mantine-datatable'; import { DataTable } from 'mantine-datatable';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSupplierPartFields } from '../../forms/CompanyForms'; import { useSupplierPartFields } from '../../forms/CompanyForms';
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import useWizard from '../../hooks/UseWizard'; import useWizard from '../../hooks/UseWizard';
import { RenderPartColumn } from '../../tables/ColumnRenderers'; import { RenderPartColumn } from '../../tables/ColumnRenderers';
import RemoveRowButton from '../buttons/RemoveRowButton'; import RemoveRowButton from '../buttons/RemoveRowButton';
import { StandaloneField } from '../forms/StandaloneField'; import { StandaloneField } from '../forms/StandaloneField';
import Expand from '../items/Expand'; import Expand from '../items/Expand';
/**
* Render the "requirements" info for a part
* This fetches the information dynamically from the API
*/
function PartRequirementsInfo({
partId,
onQuantityChange
}: {
partId: number | string;
onQuantityChange?: (quantity: number) => void;
}) {
const [requiredQuantity, setRequiredQuantity] = useState<number>(0);
// Notify parent component of quantity change
useEffect(() => {
onQuantityChange?.(requiredQuantity);
}, [requiredQuantity]);
const requirements = useInstance({
endpoint: ApiEndpoints.part_requirements,
pk: partId,
hasPrimaryKey: true,
defaultValue: {}
});
const widget = useMemo(() => {
if (
requirements.instanceQuery.isFetching ||
requirements.instanceQuery.isLoading
) {
return <Loader size='sm' />;
}
if (requirements.instanceQuery.isError) {
return (
<Tooltip label={t`Error fetching part requirements`}>
<ActionIcon variant='transparent' color='red'>
<IconExclamationCircle />
</ActionIcon>
</Tooltip>
);
}
// Calculate the total requirements
const buildRequirements =
requirements.instance?.required_for_build_orders || 0;
const salesRequirements =
requirements.instance?.required_for_sales_orders || 0;
const totalRequirements = buildRequirements + salesRequirements;
const building = requirements.instance?.building || 0;
const ordering = requirements.instance?.ordering || 0;
const incoming = building + ordering;
const inStock = requirements.instance?.total_stock || 0;
const required = Math.max(0, totalRequirements - inStock - incoming);
setRequiredQuantity(required);
return (
<HoverCard position='bottom-end'>
<HoverCard.Target>
<ActionIcon
variant='transparent'
color={required > 0 ? 'blue' : 'green'}
size='sm'
>
<IconInfoCircle />
</ActionIcon>
</HoverCard.Target>
<HoverCard.Dropdown>
<Stack gap='xs'>
<Text>{t`Requirements`}</Text>
<Divider />
{buildRequirements > 0 && (
<Group justify='space-between'>
<Text size='xs'>{t`Build Requirements`}</Text>
<Text size='xs'>{formatDecimal(buildRequirements)}</Text>
</Group>
)}
{salesRequirements > 0 && (
<Group justify='space-between'>
<Text size='xs'>{t`Sales Requirements`}</Text>
<Text size='xs'>{formatDecimal(salesRequirements)}</Text>
</Group>
)}
{inStock > 0 && (
<Group justify='space-between'>
<Text size='xs'>{t`In Stock`}</Text>
<Text size='xs'>{formatDecimal(inStock)}</Text>
</Group>
)}
{ordering > 0 && (
<Group justify='space-between'>
<Text size='xs'>{t`On Order`}</Text>
<Text size='xs'>{formatDecimal(ordering)}</Text>
</Group>
)}
{building > 0 && (
<Group justify='space-between'>
<Text size='xs'>{t`In Production`}</Text>
<Text size='xs'>{formatDecimal(building)}</Text>
</Group>
)}
<Group justify='space-between'>
<Text size='xs'>{t`Required Quantity`}</Text>
<Text size='xs'>{formatDecimal(required)}</Text>
</Group>
</Stack>
</HoverCard.Dropdown>
</HoverCard>
);
}, [
requirements.instanceQuery.isFetching,
requirements.instanceQuery.isLoading,
requirements.instanceQuery.isError,
requirements.instance,
setRequiredQuantity
]);
return widget;
}
/** /**
* Attributes for each selected part * Attributes for each selected part
* - part: The part instance * - part: The part instance
@@ -123,7 +264,10 @@ function SelectPartsStep({
width: '1%', width: '1%',
render: (record: PartOrderRecord) => ( render: (record: PartOrderRecord) => (
<Group gap='xs' wrap='nowrap' justify='left'> <Group gap='xs' wrap='nowrap' justify='left'>
<RemoveRowButton onClick={() => onRemovePart(record.part)} /> <RemoveRowButton
tooltipAlignment={'top-start'}
onClick={() => onRemovePart(record.part)}
/>
</Group> </Group>
) )
}, },
@@ -162,6 +306,7 @@ function SelectPartsStep({
filters: { filters: {
part: record.part.pk, part: record.part.pk,
active: true, active: true,
part_detail: true,
supplier_detail: true supplier_detail: true
} }
}} }}
@@ -169,7 +314,7 @@ function SelectPartsStep({
</Expand> </Expand>
<AddItemButton <AddItemButton
tooltip={t`New supplier part`} tooltip={t`New supplier part`}
tooltipAlignment='top' tooltipAlignment='top-end'
onClick={() => { onClick={() => {
setSelectedRecord(record); setSelectedRecord(record);
newSupplierPart.open(); newSupplierPart.open();
@@ -207,7 +352,7 @@ function SelectPartsStep({
</Expand> </Expand>
<AddItemButton <AddItemButton
tooltip={t`New purchase order`} tooltip={t`New purchase order`}
tooltipAlignment='top' tooltipAlignment='top-end'
disabled={!record.supplier_part?.pk} disabled={!record.supplier_part?.pk}
onClick={() => { onClick={() => {
setSelectedRecord(record); setSelectedRecord(record);
@@ -220,21 +365,29 @@ function SelectPartsStep({
{ {
accessor: 'quantity', accessor: 'quantity',
title: t`Quantity`, title: t`Quantity`,
width: 125, width: 150,
render: (record: PartOrderRecord) => ( render: (record: PartOrderRecord) => (
<StandaloneField <Group gap='xs' wrap='nowrap'>
fieldName='quantity' <StandaloneField
hideLabels={true} fieldName='quantity'
error={record.errors?.quantity} hideLabels={true}
fieldDefinition={{ error={record.errors?.quantity}
field_type: 'number', fieldDefinition={{
required: true, field_type: 'number',
value: record.quantity, required: true,
onValueChange: (value) => { value: record.quantity,
onSelectQuantity(record.part.pk, value); onValueChange: (value) => {
onSelectQuantity(record.part.pk, value);
}
}}
/>
<PartRequirementsInfo
partId={record.part.pk}
onQuantityChange={(quantity: number) =>
onSelectQuantity(record.part.pk, quantity)
} }
}} />
/> </Group>
) )
}, },
{ {
@@ -255,7 +408,7 @@ function SelectPartsStep({
} }
icon={<IconShoppingCart />} icon={<IconShoppingCart />}
tooltip={t`Add to selected purchase order`} tooltip={t`Add to selected purchase order`}
tooltipAlignment='top' tooltipAlignment='top-end'
color='blue' color='blue'
/> />
</Group> </Group>

View File

@@ -870,8 +870,6 @@ export default function BuildLineTable({
*/ */
const formatRecords = useCallback( const formatRecords = useCallback(
(records: any[]): any[] => { (records: any[]): any[] => {
console.log('format records:', records);
return records.map((record) => { return records.map((record) => {
let allocations = [...record.allocations]; let allocations = [...record.allocations];