2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-11-13 19:36:46 +00:00
Files
InvenTree/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx
github-actions[bot] 1d2700fa6f [UI] Consume tracked (#10422) (#10424)
* Prevent manual consumption of tracked stock

* Prevent manual consuming of trackable items

(cherry picked from commit b0a60ed963)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-09-29 14:01:43 +10:00

321 lines
8.5 KiB
TypeScript

import { t } from '@lingui/core/macro';
import { useCallback, useMemo, useState } from 'react';
import {
type RowAction,
RowDeleteAction,
RowEditAction
} from '@lib/components/RowActions';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import { ActionButton } from '@lib/index';
import type { TableFilter } from '@lib/types/Filters';
import type { TableColumn } from '@lib/types/Tables';
import { IconCircleDashedCheck } from '@tabler/icons-react';
import { useConsumeBuildItemsForm } from '../../forms/BuildForms';
import type { StockOperationProps } from '../../forms/StockForms';
import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import {
DecimalColumn,
LocationColumn,
PartColumn,
ReferenceColumn,
StatusColumn
} from '../ColumnRenderers';
import { IncludeVariantsFilter, StockLocationFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
/**
* Render a table of allocated stock for a build.
*/
export default function BuildAllocatedStockTable({
buildId,
stockId,
partId,
showBuildInfo,
showPartInfo,
allowEdit,
modelTarget,
modelField
}: Readonly<{
buildId?: number;
stockId?: number;
partId?: number;
showPartInfo?: boolean;
showBuildInfo?: boolean;
allowEdit?: boolean;
modelTarget?: ModelType;
modelField?: string;
}>) {
const user = useUserState();
const table = useTable(
!!partId ? 'buildallocatedstock-part' : 'buildallocatedstock'
);
const tableFilters: TableFilter[] = useMemo(() => {
const filters: TableFilter[] = [
{
name: 'tracked',
label: t`Allocated to Output`,
description: t`Show items allocated to a build output`
}
];
if (!!partId) {
filters.push(IncludeVariantsFilter());
}
filters.push(StockLocationFilter());
return filters;
}, [partId]);
const tableColumns: TableColumn[] = useMemo(() => {
return [
ReferenceColumn({
accessor: 'build_detail.reference',
title: t`Build Order`,
switchable: false,
hidden: showBuildInfo != true
}),
{
accessor: 'build_detail.title',
title: t`Description`,
hidden: showBuildInfo != true
},
StatusColumn({
accessor: 'build_detail.status',
model: ModelType.build,
title: t`Order Status`,
hidden: showBuildInfo != true
}),
PartColumn({
hidden: !showPartInfo,
switchable: false
}),
{
accessor: 'part_detail.IPN',
ordering: 'IPN',
hidden: !showPartInfo,
title: t`IPN`,
sortable: true,
switchable: true
},
{
hidden: !showPartInfo,
accessor: 'bom_reference',
title: t`Reference`,
sortable: true,
switchable: true
},
{
accessor: 'serial',
title: t`Serial Number`,
sortable: false,
switchable: true,
render: (record: any) => record?.stock_item_detail?.serial
},
{
accessor: 'batch',
title: t`Batch Code`,
sortable: false,
switchable: true,
render: (record: any) => record?.stock_item_detail?.batch
},
DecimalColumn({
accessor: 'available',
title: t`Available`
}),
DecimalColumn({
accessor: 'quantity',
title: t`Allocated`,
sortable: true,
switchable: false
}),
LocationColumn({
accessor: 'location_detail',
switchable: true,
sortable: true
}),
{
accessor: 'install_into',
title: t`Build Output`,
sortable: true
},
{
accessor: 'sku',
title: t`Supplier Part`,
render: (record: any) => record?.supplier_part_detail?.SKU,
sortable: true
}
];
}, []);
const [selectedItemId, setSelectedItemId] = useState<number>(0);
const editItem = useEditApiFormModal({
pk: selectedItemId,
url: ApiEndpoints.build_item_list,
title: t`Edit Stock Allocation`,
fields: {
stock_item: {
disabled: true
},
quantity: {}
},
table: table
});
const deleteItem = useDeleteApiFormModal({
pk: selectedItemId,
url: ApiEndpoints.build_item_list,
title: t`Delete Stock Allocation`,
table: table
});
const [selectedItems, setSelectedItems] = useState<any[]>([]);
const itemsToConsume = useMemo(() => {
return selectedItems.filter((item) => !item.part_detail?.trackable);
}, [selectedItems]);
const consumeStock = useConsumeBuildItemsForm({
buildId: buildId ?? 0,
allocatedItems: itemsToConsume,
onFormSuccess: () => {
table.clearSelectedRecords();
table.refreshTable();
}
});
const stockOperationProps: StockOperationProps = useMemo(() => {
// Extract stock items from the selected records
// Note that the table is actually a list of BuildItem instances,
// so we need to reconstruct the stock item details
const stockItems: any[] = table.selectedRecords
.filter((item: any) => !!item.stock_item_detail)
.map((item: any) => {
return {
...item.stock_item_detail,
part_detail: item.part_detail,
location_detail: item.location_detail
};
});
return {
items: stockItems,
model: ModelType.stockitem,
refresh: table.refreshTable
};
}, [table.selectedRecords, table.refreshTable]);
const stockAdjustActions = useStockAdjustActions({
formProps: stockOperationProps,
merge: false,
assign: false,
delete: false,
add: false,
count: false,
remove: false
});
const rowActions = useCallback(
(record: any): RowAction[] => {
const part = record.part_detail ?? {};
const trackable: boolean = part?.trackable ?? false;
return [
{
color: 'green',
icon: <IconCircleDashedCheck />,
title: t`Consume`,
tooltip: t`Consume Stock`,
hidden: !buildId || trackable || !user.hasChangeRole(UserRoles.build),
onClick: () => {
setSelectedItems([record]);
consumeStock.open();
}
},
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.build),
onClick: () => {
setSelectedItemId(record.pk);
editItem.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.build),
onClick: () => {
setSelectedItemId(record.pk);
deleteItem.open();
}
})
];
},
[user]
);
const tableActions = useMemo(() => {
return [
stockAdjustActions.dropdown,
<ActionButton
key='consume-stock'
icon={<IconCircleDashedCheck />}
tooltip={t`Consume Stock`}
hidden={!user.hasChangeRole(UserRoles.build)}
disabled={table.selectedRecords.length == 0}
color='green'
onClick={() => {
setSelectedItems(table.selectedRecords);
consumeStock.open();
}}
/>
];
}, [user, table.selectedRecords, stockAdjustActions.dropdown]);
return (
<>
{editItem.modal}
{deleteItem.modal}
{consumeStock.modal}
{stockAdjustActions.modals.map((modal) => modal.modal)}
<InvenTreeTable
tableState={table}
url={apiUrl(ApiEndpoints.build_item_list)}
columns={tableColumns}
props={{
params: {
build: buildId,
part: partId,
stock_item: stockId,
build_detail: showBuildInfo ?? false,
part_detail: showPartInfo ?? false,
location_detail: true,
stock_detail: true,
supplier_detail: true
},
enableBulkDelete: allowEdit && user.hasDeleteRole(UserRoles.build),
enableDownload: true,
enableSelection: allowEdit && user.hasDeleteRole(UserRoles.build),
rowActions: rowActions,
tableActions: tableActions,
tableFilters: tableFilters,
enableLabels: true,
enableReports: true,
printingAccessor: 'stock_item',
modelField: modelField ?? 'stock_item',
modelType: modelTarget ?? ModelType.stockitem
}}
/>
</>
);
}