mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 13:28:49 +00:00
[PUI] Installed items (#8202)
* Improve checks if "installed items" panel should be displayed * Adds ability to exclude a given field from form data * Implement form for installing stock item * Add placeholder for uninstall action * Add "note" field * Add PUI form to uninstall stock item
This commit is contained in:
parent
a5f2273e14
commit
392624cb84
@ -820,9 +820,9 @@ class InstallStockItemSerializer(serializers.Serializer):
|
||||
quantity = data.get('quantity', stock_item.quantity)
|
||||
|
||||
if quantity > stock_item.quantity:
|
||||
raise ValidationError(
|
||||
_('Quantity to install must not exceed available quantity')
|
||||
)
|
||||
raise ValidationError({
|
||||
'quantity': _('Quantity to install must not exceed available quantity')
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
|
@ -397,11 +397,13 @@ export function ApiForm({
|
||||
data = props.processFormData(data);
|
||||
}
|
||||
|
||||
let dataForm = new FormData();
|
||||
let jsonData = { ...data };
|
||||
let formData = new FormData();
|
||||
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
let value: any = data[key];
|
||||
let field_type = fields[key]?.field_type;
|
||||
let exclude = fields[key]?.exclude;
|
||||
|
||||
if (field_type == 'file upload' && !!value) {
|
||||
hasFiles = true;
|
||||
@ -418,15 +420,18 @@ export function ApiForm({
|
||||
}
|
||||
}
|
||||
|
||||
if (value != undefined) {
|
||||
dataForm.append(key, value);
|
||||
if (exclude) {
|
||||
// Remove the field from the data
|
||||
delete jsonData[key];
|
||||
} else if (value != undefined) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return api({
|
||||
method: method,
|
||||
url: url,
|
||||
data: hasFiles ? dataForm : data,
|
||||
data: hasFiles ? formData : jsonData,
|
||||
timeout: props.timeout,
|
||||
headers: {
|
||||
'Content-Type': hasFiles ? 'multipart/form-data' : 'application/json'
|
||||
|
@ -41,6 +41,7 @@ export type ApiFormAdjustFilterType = {
|
||||
* @param required : Whether the field is required
|
||||
* @param hidden : Whether the field is hidden
|
||||
* @param disabled : Whether the field is disabled
|
||||
* @param exclude : Whether to exclude the field from the submitted data
|
||||
* @param placeholder : The placeholder text to display
|
||||
* @param description : The description to display for the field
|
||||
* @param preFieldContent : Content to render before the field
|
||||
@ -84,6 +85,7 @@ export type ApiFormFieldType = {
|
||||
choices?: any[];
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
exclude?: boolean;
|
||||
read_only?: boolean;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
|
@ -134,6 +134,7 @@ export enum ApiEndpoints {
|
||||
stock_assign = 'stock/assign/',
|
||||
stock_status = 'stock/status/',
|
||||
stock_install = 'stock/:id/install/',
|
||||
stock_uninstall = 'stock/:id/uninstall/',
|
||||
stock_serialize = 'stock/:id/serialize/',
|
||||
build_test_statistics = 'test-statistics/by-build/:id/',
|
||||
part_test_statistics = 'test-statistics/by-part/:id/',
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
IconUsersGroup
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { Suspense, useMemo, useState } from 'react';
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { api } from '../App';
|
||||
import { ActionButton } from '../components/buttons/ActionButton';
|
||||
@ -215,6 +215,78 @@ export function useCreateStockItem() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Form set for manually removing (uninstalling) a StockItem from an existing StockItem
|
||||
*/
|
||||
export function useStockItemUninstallFields(): ApiFormFieldSet {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
location: {
|
||||
filters: {
|
||||
structural: false
|
||||
}
|
||||
},
|
||||
note: {}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form set for manually installing a StockItem into an existing StockItem
|
||||
*/
|
||||
export function useStockItemInstallFields({
|
||||
stockItem
|
||||
}: {
|
||||
stockItem: any;
|
||||
}): ApiFormFieldSet {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const [selectedPart, setSelectedPart] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedPart(null);
|
||||
}, [stockItem]);
|
||||
|
||||
return useMemo(() => {
|
||||
// Note: The 'part' field is not a part of the API endpoint, so we construct it manually
|
||||
return {
|
||||
part: {
|
||||
field_type: 'related field',
|
||||
required: true,
|
||||
exclude: true,
|
||||
label: t`Part`,
|
||||
description: t`Select the part to install`,
|
||||
model: ModelType.part,
|
||||
api_url: apiUrl(ApiEndpoints.part_list),
|
||||
onValueChange: (value) => {
|
||||
setSelectedPart(value);
|
||||
},
|
||||
filters: {
|
||||
trackable: true,
|
||||
in_bom_for: globalSettings.isSet('STOCK_ENFORCE_BOM_INSTALLATION')
|
||||
? stockItem.part
|
||||
: undefined
|
||||
}
|
||||
},
|
||||
stock_item: {
|
||||
disabled: !selectedPart,
|
||||
filters: {
|
||||
part_detail: true,
|
||||
in_stock: true,
|
||||
available: true,
|
||||
tracked: true,
|
||||
part: selectedPart ? selectedPart : undefined
|
||||
}
|
||||
},
|
||||
quantity: {},
|
||||
note: {}
|
||||
};
|
||||
}, [globalSettings, selectedPart, stockItem]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form set for serializing an existing StockItem
|
||||
*/
|
||||
export function useStockItemSerializeFields({
|
||||
partId,
|
||||
trackable
|
||||
|
@ -11,9 +11,11 @@ import {
|
||||
IconPaperclip,
|
||||
IconSitemap
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ReactNode, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
@ -57,6 +59,7 @@ import {
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||
@ -288,7 +291,7 @@ export default function StockDetail() {
|
||||
);
|
||||
}, [stockitem, instanceQuery]);
|
||||
|
||||
const showBuildAllocations = useMemo(() => {
|
||||
const showBuildAllocations: boolean = useMemo(() => {
|
||||
// Determine if "build allocations" should be shown for this stock item
|
||||
return (
|
||||
stockitem?.part_detail?.component && // Must be a "component"
|
||||
@ -297,10 +300,57 @@ export default function StockDetail() {
|
||||
); // Must not be installed into another item
|
||||
}, [stockitem]);
|
||||
|
||||
const showSalesAlloctions = useMemo(() => {
|
||||
const showSalesAlloctions: boolean = useMemo(() => {
|
||||
return stockitem?.part_detail?.salable;
|
||||
}, [stockitem]);
|
||||
|
||||
// API query to determine if this stock item has trackable BOM items
|
||||
const trackedBomItemQuery = useQuery({
|
||||
queryKey: ['tracked-bom-item', stockitem.pk, stockitem.part],
|
||||
queryFn: () => {
|
||||
if (
|
||||
!stockitem.pk ||
|
||||
!stockitem.part ||
|
||||
!stockitem.part_detail?.assembly
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return api
|
||||
.get(apiUrl(ApiEndpoints.bom_list), {
|
||||
params: {
|
||||
part: stockitem.part,
|
||||
sub_part_trackable: true,
|
||||
limit: 1
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
return response.data.count > 0;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const showInstalledItems: boolean = useMemo(() => {
|
||||
if (stockitem?.installed_items) {
|
||||
// There are installed items in this stock item
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trackedBomItemQuery.data != null) {
|
||||
return trackedBomItemQuery.data;
|
||||
}
|
||||
|
||||
// Fall back to whether this is an assembly or not
|
||||
return stockitem?.part_detail?.assembly;
|
||||
}, [trackedBomItemQuery, stockitem]);
|
||||
|
||||
const stockPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
@ -380,8 +430,8 @@ export default function StockDetail() {
|
||||
name: 'installed_items',
|
||||
label: t`Installed Items`,
|
||||
icon: <IconBoxPadding />,
|
||||
hidden: !stockitem?.part_detail?.assembly,
|
||||
content: <InstalledItemsTable parentId={stockitem.pk} />
|
||||
hidden: !showInstalledItems,
|
||||
content: <InstalledItemsTable stockItem={stockitem} />
|
||||
},
|
||||
{
|
||||
name: 'child_items',
|
||||
@ -421,7 +471,14 @@ export default function StockDetail() {
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [stockitem, id, user]);
|
||||
}, [
|
||||
showSalesAlloctions,
|
||||
showBuildAllocations,
|
||||
showInstalledItems,
|
||||
stockitem,
|
||||
id,
|
||||
user
|
||||
]);
|
||||
|
||||
const breadcrumbs = useMemo(
|
||||
() => [
|
||||
|
@ -1,7 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Skeleton } from '@mantine/core';
|
||||
import { IconUnlink } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
import {
|
||||
useStockItemInstallFields,
|
||||
useStockItemUninstallFields
|
||||
} from '../../forms/StockForms';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
@ -10,13 +20,42 @@ import { PartColumn, StatusColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
export default function InstalledItemsTable({
|
||||
parentId
|
||||
stockItem
|
||||
}: Readonly<{
|
||||
parentId?: number | string;
|
||||
stockItem: any;
|
||||
}>) {
|
||||
const table = useTable('stock_item_install');
|
||||
const user = useUserState();
|
||||
|
||||
const installItemFields = useStockItemInstallFields({
|
||||
stockItem: stockItem
|
||||
});
|
||||
|
||||
const installItem = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.stock_install),
|
||||
pk: stockItem.pk,
|
||||
title: t`Install Item`,
|
||||
table: table,
|
||||
successMessage: t`Item installed`,
|
||||
fields: installItemFields
|
||||
});
|
||||
|
||||
const [selectedRecord, setSelectedRecord] = useState<any>({});
|
||||
|
||||
const uninstallItemFields = useStockItemUninstallFields();
|
||||
|
||||
const uninstallItem = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.stock_uninstall),
|
||||
pk: selectedRecord.pk,
|
||||
title: t`Uninstall Item`,
|
||||
table: table,
|
||||
successMessage: t`Item uninstalled`,
|
||||
fields: uninstallItemFields,
|
||||
initialData: {
|
||||
location: stockItem.location ?? stockItem.part_detail?.default_location
|
||||
}
|
||||
});
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
@ -46,22 +85,61 @@ export default function InstalledItemsTable({
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [];
|
||||
}, [user]);
|
||||
return [
|
||||
<AddItemButton
|
||||
tooltip={t`Install Item`}
|
||||
onClick={() => {
|
||||
installItem.open();
|
||||
}}
|
||||
hidden={
|
||||
!user.hasChangeRole(UserRoles.stock) ||
|
||||
stockItem.is_building ||
|
||||
stockItem.part_detail?.assembly != true
|
||||
}
|
||||
/>
|
||||
];
|
||||
}, [stockItem, user]);
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
return [
|
||||
{
|
||||
title: t`Uninstall`,
|
||||
tooltip: t`Uninstall stock item`,
|
||||
onClick: () => {
|
||||
setSelectedRecord(record);
|
||||
uninstallItem.open();
|
||||
},
|
||||
icon: <IconUnlink />,
|
||||
hidden: !user.hasChangeRole(UserRoles.stock)
|
||||
}
|
||||
];
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
tableActions: tableActions,
|
||||
modelType: ModelType.stockitem,
|
||||
params: {
|
||||
belongs_to: parentId,
|
||||
part_detail: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{installItem.modal}
|
||||
{uninstallItem.modal}
|
||||
{stockItem.pk ? (
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
tableActions: tableActions,
|
||||
rowActions: rowActions,
|
||||
modelType: ModelType.stockitem,
|
||||
params: {
|
||||
belongs_to: stockItem.pk,
|
||||
part_detail: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user