import { t } from '@lingui/macro'; import { Container, Flex, FocusTrap, Group, Modal, NumberInput, Table, TextInput } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconAddressBook, IconCalendar, IconCoins, IconCurrencyDollar, IconHash, IconLink, IconList, IconNotes, IconSitemap, IconUser, IconUsers } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { useEffect, useMemo, useState } from 'react'; import { api } from '../App'; import { ActionButton } from '../components/buttons/ActionButton'; import { StandaloneField } from '../components/forms/StandaloneField'; import { ApiFormAdjustFilterType, ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { TableFieldExtraRow } from '../components/forms/fields/TableField'; import { Thumbnail } from '../components/images/Thumbnail'; import { ProgressBar } from '../components/items/ProgressBar'; import { StylishText } from '../components/items/StylishText'; import { ApiEndpoints } from '../enums/ApiEndpoints'; import { ModelType } from '../enums/ModelType'; import { InvenTreeIcon } from '../functions/icons'; import { useCreateApiFormModal } from '../hooks/UseForm'; import { useBatchCodeGenerator, useSerialNumberGenerator } from '../hooks/UseGenerator'; import { apiUrl } from '../states/ApiState'; /* * Construct a set of fields for creating / editing a PurchaseOrderLineItem instance */ export function usePurchaseOrderLineItemFields({ supplierId, orderId, create }: { supplierId?: number; orderId?: number; create?: boolean; }) { const [purchasePrice, setPurchasePrice] = useState(''); const [autoPricing, setAutoPricing] = useState(true); useEffect(() => { if (autoPricing) { setPurchasePrice(''); } }, [autoPricing]); useEffect(() => { setAutoPricing(purchasePrice === ''); }, [purchasePrice]); const fields = useMemo(() => { const fields: ApiFormFieldSet = { order: { filters: { supplier_detail: true }, disabled: true }, part: { filters: { part_detail: true, supplier_detail: true, active: true, part_active: true }, adjustFilters: (adjust: ApiFormAdjustFilterType) => { return { ...adjust.filters, supplier: supplierId }; } }, quantity: {}, reference: {}, purchase_price: { icon: , value: purchasePrice, onValueChange: setPurchasePrice }, purchase_price_currency: { icon: }, auto_pricing: { value: autoPricing, onValueChange: setAutoPricing }, target_date: { icon: }, destination: { icon: }, notes: { icon: }, link: { icon: } }; if (create) { fields['merge_items'] = {}; } return fields; }, [create, orderId, supplierId, autoPricing, purchasePrice]); return fields; } /** * Construct a set of fields for creating / editing a PurchaseOrder instance */ export function usePurchaseOrderFields(): ApiFormFieldSet { return useMemo(() => { return { reference: { icon: }, description: {}, supplier: { filters: { is_supplier: true, active: true } }, supplier_reference: {}, project_code: { icon: }, order_currency: { icon: }, target_date: { icon: }, link: {}, contact: { icon: , adjustFilters: (value: ApiFormAdjustFilterType) => { return { ...value.filters, company: value.data.supplier }; } }, address: { icon: , adjustFilters: (value: ApiFormAdjustFilterType) => { return { ...value.filters, company: value.data.supplier }; } }, responsible: { filters: { is_active: true }, icon: } }; }, []); } /** * Render a table row for a single TableField entry */ function LineItemFormRow({ input, record, statuses }: { input: any; record: any; statuses: any; }) { // Barcode Modal state const [opened, { open, close }] = useDisclosure(false); // Location value const [location, setLocation] = useState( input.item.location ?? record.part_detail.default_location ?? record.part_detail.category_default_location ); const [locationOpen, locationHandlers] = useDisclosure( location ? true : false, { onClose: () => input.changeFn(input.idx, 'location', null), onOpen: () => input.changeFn(input.idx, 'location', location) } ); // Change form value when state is altered useEffect(() => { input.changeFn(input.idx, 'location', location); }, [location]); const batchCodeGenerator = useBatchCodeGenerator((value: any) => { if (!batchCode) { setBatchCode(value); } }); const serialNumberGenerator = useSerialNumberGenerator((value: any) => { if (!serials) { setSerials(value); } }); const [packagingOpen, packagingHandlers] = useDisclosure(false, { onClose: () => { input.changeFn(input.idx, 'packaging', undefined); } }); const [noteOpen, noteHandlers] = useDisclosure(false, { onClose: () => { input.changeFn(input.idx, 'note', undefined); } }); // State for serializing const [batchCode, setBatchCode] = useState(''); const [serials, setSerials] = useState(''); const [batchOpen, batchHandlers] = useDisclosure(false, { onClose: () => { input.changeFn(input.idx, 'batch_code', undefined); input.changeFn(input.idx, 'serial_numbers', ''); }, onOpen: () => { // Generate a new batch code batchCodeGenerator.update({ part: record?.supplier_part_detail?.part, order: record?.order }); // Generate new serial numbers serialNumberGenerator.update({ part: record?.supplier_part_detail?.part, quantity: input.item.quantity }); } }); // Status value const [statusOpen, statusHandlers] = useDisclosure(false, { onClose: () => input.changeFn(input.idx, 'status', 10) }); // Barcode value const [barcodeInput, setBarcodeInput] = useState(''); const [barcode, setBarcode] = useState(null); // Change form value when state is altered useEffect(() => { input.changeFn(input.idx, 'barcode', barcode); }, [barcode]); // Update location field description on state change useEffect(() => { if (!opened) { return; } const timeoutId = setTimeout(() => { setBarcode(barcodeInput.length ? barcodeInput : null); close(); setBarcodeInput(''); }, 500); return () => clearTimeout(timeoutId); }, [barcodeInput]); // Info string with details about certain selected locations const locationDescription = useMemo(() => { let text = t`Choose Location`; if (location === null) { return text; } // Selected location is order line destination if (location === record.destination) { return t`Item Destination selected`; } // Selected location is base part's category default location if ( !record.destination && !record.destination_detail && location === record.part_detail.category_default_location ) { return t`Part category default location selected`; } // Selected location is identical to already received stock for this line if ( !record.destination && record.destination_detail && location === record.destination_detail.pk && record.received > 0 ) { return t`Received stock location selected`; } // Selected location is base part's default location if (location === record.part_detail.default_location) { return t`Default location selected`; } return text; }, [location]); return ( <> } > setBarcodeInput(e.target.value)} />
{record.part_detail.name}
{record.supplier_part_detail.SKU} input.changeFn(input.idx, 'quantity', value)} /> locationHandlers.toggle()} icon={} tooltip={t`Set Location`} tooltipAlignment="top" variant={locationOpen ? 'filled' : 'transparent'} /> batchHandlers.toggle()} icon={} tooltip={t`Assign Batch Code${ record.trackable && ' and Serial Numbers' }`} tooltipAlignment="top" variant={batchOpen ? 'filled' : 'transparent'} /> } tooltip={t`Adjust Packaging`} onClick={() => packagingHandlers.toggle()} variant={packagingOpen ? 'filled' : 'transparent'} /> statusHandlers.toggle()} icon={} tooltip={t`Change Status`} tooltipAlignment="top" variant={statusOpen ? 'filled' : 'transparent'} /> } tooltip={t`Add Note`} tooltipAlignment="top" variant={noteOpen ? 'filled' : 'transparent'} onClick={() => noteHandlers.toggle()} /> {barcode ? ( } tooltip={t`Unlink Barcode`} tooltipAlignment="top" variant="filled" color="red" onClick={() => setBarcode(null)} /> ) : ( } tooltip={t`Scan Barcode`} tooltipAlignment="top" variant="transparent" onClick={() => open()} /> )} input.removeFn(input.idx)} icon={} tooltip={t`Remove item from list`} tooltipAlignment="top" color="red" />
{locationOpen && ( { setLocation(value); }, description: locationDescription, value: location, label: t`Location`, icon: }} defaultValue={ record.destination ?? (record.destination_detail ? record.destination_detail.pk : null) } /> {(record.part_detail.default_location || record.part_detail.category_default_location) && ( } tooltip={t`Store at default location`} onClick={() => setLocation( record.part_detail.default_location ?? record.part_detail.category_default_location ) } tooltipAlignment="top" /> )} {record.destination && ( } tooltip={t`Store at line item destination `} onClick={() => setLocation(record.destination)} tooltipAlignment="top" /> )} {!record.destination && record.destination_detail && record.received > 0 && ( } tooltip={t`Store with already received stock`} onClick={() => setLocation(record.destination_detail.pk)} tooltipAlignment="top" /> )} )} input.changeFn(input.idx, 'batch', value)} fieldDefinition={{ field_type: 'string', label: t`Batch Code`, value: batchCode }} /> input.changeFn(input.idx, 'serial_numbers', value) } fieldDefinition={{ field_type: 'string', label: t`Serial numbers`, value: serials }} /> input.changeFn(input.idx, 'packaging', value)} fieldDefinition={{ field_type: 'string', label: t`Packaging` }} defaultValue={record?.supplier_part_detail?.packaging} /> input.changeFn(input.idx, 'status', value)} fieldDefinition={{ field_type: 'choice', api_url: apiUrl(ApiEndpoints.stock_status), choices: statuses, label: t`Status` }} /> input.changeFn(input.idx, 'note', value)} fieldDefinition={{ field_type: 'string', label: t`Note` }} /> ); } type LineFormHandlers = { onOpen?: () => void; onClose?: () => void; }; type LineItemsForm = { items: any[]; orderPk: number; formProps?: LineFormHandlers; }; export function useReceiveLineItems(props: LineItemsForm) { const { data } = useQuery({ queryKey: ['stock', 'status'], queryFn: async () => { return api.get(apiUrl(ApiEndpoints.stock_status)).then((response) => { if (response.status === 200) { const entries = Object.values(response.data.values); const mapped = entries.map((item: any) => { return { value: item.key, display_name: item.label }; }); return mapped; } }); } }); const records = Object.fromEntries( props.items.map((item) => [item.pk, item]) ); const filteredItems = props.items.filter( (elem) => elem.quantity !== elem.received ); const fields: ApiFormFieldSet = { id: { value: props.orderPk, hidden: true }, items: { field_type: 'table', value: filteredItems.map((elem, idx) => { return { line_item: elem.pk, location: elem.destination ?? elem.destination_detail?.pk ?? null, quantity: elem.quantity - elem.received, batch_code: '', serial_numbers: '', status: 10, barcode: null }; }), modelRenderer: (instance) => { const record = records[instance.item.line_item]; return ( ); }, headers: [t`Part`, t`SKU`, t`Received`, t`Quantity`, t`Actions`] }, location: { filters: { structural: false } } }; const url = apiUrl(ApiEndpoints.purchase_order_receive, null, { id: props.orderPk }); return useCreateApiFormModal({ ...props.formProps, url: url, title: t`Receive Line Items`, fields: fields, initialData: { location: null }, size: 'xl' }); }