2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 03:56:43 +00:00
InvenTree/src/frontend/src/tables/stock/StockItemTable.tsx
Oliver 0cb4e8ec1c
[PUI] table fixes (#6764)
* Impove link rendering in attachment table

* Add "updateRecord" hook for useTable

* Refactoring

Make use of new table.updateRecord method

* Refactor model row clicks

- Just need to provide a model type

* Fix BuildLineTable

* Re-add required imports

* Remove hard-coded paths
2024-03-20 10:56:32 +00:00

524 lines
14 KiB
TypeScript

import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import { formatCurrency, renderDate } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import {
StockOperationProps,
useAddStockItem,
useAssignStockItem,
useChangeStockStatus,
useCountStockItem,
useDeleteStockItem,
useMergeStockItem,
useRemoveStockItem,
useStockFields,
useTransferStockItem
} from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import {
DescriptionColumn,
PartColumn,
StatusColumn
} from '../ColumnRenderers';
import { StatusFilterOptions, TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
/**
* Construct a list of columns for the stock item table
*/
function stockItemTableColumns(): TableColumn[] {
return [
{
accessor: 'part',
sortable: true,
render: (record: any) => PartColumn(record?.part_detail)
},
DescriptionColumn({
accessor: 'part_detail.description'
}),
{
accessor: 'quantity',
ordering: 'stock',
sortable: true,
title: t`Stock`,
render: (record) => {
// TODO: Push this out into a custom renderer
let quantity = record?.quantity ?? 0;
let allocated = record?.allocated ?? 0;
let available = quantity - allocated;
let text = quantity;
let part = record?.part_detail ?? {};
let extra: ReactNode[] = [];
let color = undefined;
if (record.serial && quantity == 1) {
text = `# ${record.serial}`;
}
if (record.is_building) {
color = 'blue';
extra.push(
<Text
key="production"
size="sm"
>{t`This stock item is in production`}</Text>
);
}
if (record.sales_order) {
extra.push(
<Text
key="sales-order"
size="sm"
>{t`This stock item has been assigned to a sales order`}</Text>
);
}
if (record.customer) {
extra.push(
<Text
key="customer"
size="sm"
>{t`This stock item has been assigned to a customer`}</Text>
);
}
if (record.belongs_to) {
extra.push(
<Text
key="belongs-to"
size="sm"
>{t`This stock item is installed in another stock item`}</Text>
);
}
if (record.consumed_by) {
extra.push(
<Text
key="consumed-by"
size="sm"
>{t`This stock item has been consumed by a build order`}</Text>
);
}
if (record.expired) {
extra.push(
<Text
key="expired"
size="sm"
>{t`This stock item has expired`}</Text>
);
} else if (record.stale) {
extra.push(
<Text key="stale" size="sm">{t`This stock item is stale`}</Text>
);
}
if (allocated > 0) {
if (allocated >= quantity) {
color = 'orange';
extra.push(
<Text
key="fully-allocated"
size="sm"
>{t`This stock item is fully allocated`}</Text>
);
} else {
extra.push(
<Text
key="partially-allocated"
size="sm"
>{t`This stock item is partially allocated`}</Text>
);
}
}
if (available != quantity) {
if (available > 0) {
extra.push(
<Text key="available" size="sm" color="orange">
{t`Available` + `: ${available}`}
</Text>
);
} else {
extra.push(
<Text
key="no-stock"
size="sm"
color="red"
>{t`No stock available`}</Text>
);
}
}
if (quantity <= 0) {
color = 'red';
extra.push(
<Text
key="depleted"
size="sm"
>{t`This stock item has been depleted`}</Text>
);
}
return (
<TableHoverCard
value={
<Group spacing="xs" position="left" noWrap={true}>
<Text color={color}>{text}</Text>
{part.units && (
<Text size="xs" color={color}>
[{part.units}]
</Text>
)}
</Group>
}
title={t`Stock Information`}
extra={extra}
/>
);
}
},
StatusColumn(ModelType.stockitem),
{
accessor: 'batch',
sortable: true
},
{
accessor: 'location',
sortable: true,
render: function (record: any) {
// TODO: Custom renderer for location
// TODO: Note, if not "In stock" we don't want to display the actual location here
return record?.location_detail?.pathstring ?? record.location ?? '-';
}
},
// TODO: stocktake column
{
accessor: 'expiry_date',
sortable: true,
switchable: true,
render: (record: any) => renderDate(record.expiry_date)
},
{
accessor: 'updated',
sortable: true,
switchable: true,
render: (record: any) => renderDate(record.updated)
},
// TODO: purchase order
// TODO: Supplier part
{
accessor: 'purchase_price',
sortable: true,
switchable: true,
render: (record: any) =>
formatCurrency(record.purchase_price, {
currency: record.purchase_price_currency
})
}
// TODO: stock value
// TODO: packaging
// TODO: notes
];
}
/**
* Construct a list of available filters for the stock item table
*/
function stockItemTableFilters(): TableFilter[] {
return [
{
name: 'active',
label: t`Active`,
description: t`Show stock for active parts`
},
{
name: 'status',
label: t`Status`,
description: t`Filter by stock status`,
choiceFunction: StatusFilterOptions(ModelType.stockitem)
},
{
name: 'assembly',
label: t`Assembly`,
description: t`Show stock for assmebled parts`
},
{
name: 'allocated',
label: t`Allocated`,
description: t`Show items which have been allocated`
},
{
name: 'available',
label: t`Available`,
description: t`Show items which are available`
},
{
name: 'cascade',
label: t`Include Sublocations`,
description: t`Include stock in sublocations`
},
{
name: 'depleted',
label: t`Depleted`,
description: t`Show depleted stock items`
},
{
name: 'in_stock',
label: t`In Stock`,
description: t`Show items which are in stock`
},
{
name: 'is_building',
label: t`In Production`,
description: t`Show items which are in production`
},
{
name: 'include_variants',
label: t`Include Variants`,
description: t`Include stock items for variant parts`
},
{
name: 'installed',
label: t`Installed`,
description: t`Show stock items which are installed in other items`
},
{
name: 'sent_to_customer',
label: t`Sent to Customer`,
description: t`Show items which have been sent to a customer`
},
{
name: 'serialized',
label: t`Is Serialized`,
description: t`Show items which have a serial number`
},
// TODO: serial
// TODO: serial_gte
// TODO: serial_lte
{
name: 'has_batch',
label: t`Has Batch Code`,
description: t`Show items which have a batch code`
},
// TODO: batch
{
name: 'tracked',
label: t`Tracked`,
description: t`Show tracked items`
},
{
name: 'has_purchase_price',
label: t`Has Purchase Price`,
description: t`Show items which have a purchase price`
},
// TODO: Expired
// TODO: stale
// TODO: expiry_date_lte
// TODO: expiry_date_gte
{
name: 'external',
label: t`External Location`,
description: t`Show items in an external location`
}
];
}
/*
* Load a table of stock items
*/
export function StockItemTable({ params = {} }: { params?: any }) {
let tableColumns = useMemo(() => stockItemTableColumns(), []);
let tableFilters = useMemo(() => stockItemTableFilters(), []);
const table = useTable('stockitems');
const user = useUserState();
const navigate = useNavigate();
const tableActionParams: StockOperationProps = useMemo(() => {
return {
items: table.selectedRecords,
model: ModelType.stockitem,
refresh: table.refreshTable
};
}, [table]);
const stockItemFields = useStockFields({ create: true });
const newStockItem = useCreateApiFormModal({
url: ApiEndpoints.stock_item_list,
title: t`Add Stock Item`,
fields: stockItemFields,
initialData: {
part: params.part,
location: params.location
},
onFormSuccess: (data: any) => {
if (data.pk) {
navigate(getDetailUrl(ModelType.stockitem, data.pk));
}
}
});
const transferStock = useTransferStockItem(tableActionParams);
const addStock = useAddStockItem(tableActionParams);
const removeStock = useRemoveStockItem(tableActionParams);
const countStock = useCountStockItem(tableActionParams);
const changeStockStatus = useChangeStockStatus(tableActionParams);
const mergeStock = useMergeStockItem(tableActionParams);
const assignStock = useAssignStockItem(tableActionParams);
const deleteStock = useDeleteStockItem(tableActionParams);
const tableActions = useMemo(() => {
let can_delete_stock = user.hasDeleteRole(UserRoles.stock);
let can_add_stock = user.hasAddRole(UserRoles.stock);
let can_add_stocktake = user.hasAddRole(UserRoles.stocktake);
let can_add_order = user.hasAddRole(UserRoles.purchase_order);
let can_change_order = user.hasChangeRole(UserRoles.purchase_order);
return [
<ActionDropdown
key="stockoperations"
icon={<InvenTreeIcon icon="stock" />}
disabled={table.selectedRecords.length === 0}
actions={[
{
name: t`Add stock`,
icon: <InvenTreeIcon icon="add" iconProps={{ color: 'green' }} />,
tooltip: t`Add a new stock item`,
disabled: !can_add_stock,
onClick: () => {
addStock.open();
}
},
{
name: t`Remove stock`,
icon: <InvenTreeIcon icon="remove" iconProps={{ color: 'red' }} />,
tooltip: t`Remove some quantity from a stock item`,
disabled: !can_add_stock,
onClick: () => {
removeStock.open();
}
},
{
name: 'Count Stock',
icon: (
<InvenTreeIcon icon="stocktake" iconProps={{ color: 'blue' }} />
),
tooltip: 'Count Stock',
disabled: !can_add_stocktake,
onClick: () => {
countStock.open();
}
},
{
name: t`Transfer stock`,
icon: (
<InvenTreeIcon icon="transfer" iconProps={{ color: 'blue' }} />
),
tooltip: t`Move Stock items to new locations`,
disabled: !can_add_stock,
onClick: () => {
transferStock.open();
}
},
{
name: t`Change stock status`,
icon: <InvenTreeIcon icon="info" iconProps={{ color: 'blue' }} />,
tooltip: t`Change the status of stock items`,
disabled: !can_add_stock,
onClick: () => {
changeStockStatus.open();
}
},
{
name: t`Merge stock`,
icon: <InvenTreeIcon icon="merge" />,
tooltip: t`Merge stock items`,
disabled: !can_add_stock,
onClick: () => {
mergeStock.open();
}
},
{
name: t`Order stock`,
icon: <InvenTreeIcon icon="buy" />,
tooltip: t`Order new stock`,
disabled: !can_add_order || !can_change_order
},
{
name: t`Assign to customer`,
icon: <InvenTreeIcon icon="customer" />,
tooltip: t`Order new stock`,
disabled: !can_add_stock,
onClick: () => {
assignStock.open();
}
},
{
name: t`Delete stock`,
icon: <InvenTreeIcon icon="delete" iconProps={{ color: 'red' }} />,
tooltip: t`Delete stock items`,
disabled: !can_delete_stock,
onClick: () => {
deleteStock.open();
}
}
]}
/>,
<AddItemButton
hidden={!user.hasAddRole(UserRoles.stock)}
tooltip={t`Add Stock Item`}
onClick={() => newStockItem.open()}
/>
];
}, [user, table]);
return (
<>
{newStockItem.modal}
{transferStock.modal}
{removeStock.modal}
{addStock.modal}
{countStock.modal}
{changeStockStatus.modal}
{mergeStock.modal}
{assignStock.modal}
{deleteStock.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.stock_item_list)}
tableState={table}
columns={tableColumns}
props={{
enableDownload: true,
enableSelection: true,
tableFilters: tableFilters,
tableActions: tableActions,
modelType: ModelType.stockitem,
params: {
...params,
part_detail: true,
location_detail: true,
supplier_part_detail: true
}
}}
/>
</>
);
}