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:
@@ -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'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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})`;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user