2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 04:25:42 +00:00

[PUI] Stock Detail (#8274)

* Add "in_stock" attribute to StockItem API

* Add "unavailable" badge to StockDetail page

* Hide stock actions menu for "unavailable" stock

* Fix renderer for StockItemTable

* refactor stock table display

* Add 'date' type details field

* Disable "expiry" information on StockDetailPage

* Icon fix

* Bump API version
This commit is contained in:
Oliver
2024-10-11 23:39:07 +11:00
committed by GitHub
parent 48bb5fd579
commit f77c8a5b5b
7 changed files with 136 additions and 78 deletions

View File

@ -15,6 +15,7 @@ import { ReactNode, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../../App';
import { formatDate } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons';
@ -50,7 +51,7 @@ type BadgeType = 'owner' | 'user' | 'group';
type ValueFormatterReturn = string | number | null | React.ReactNode;
type StringDetailField = {
type: 'string' | 'text';
type: 'string' | 'text' | 'date';
unit?: boolean;
};
@ -174,6 +175,10 @@ function NameBadge({
);
}
function DateValue(props: Readonly<FieldProps>) {
return <Text size="sm">{formatDate(props.field_value?.toString())}</Text>;
}
/**
* Renders the value of a 'string' or 'text' field.
* If owner is defined, only renders a badge
@ -346,6 +351,8 @@ export function DetailsTableField({
return ProgressBarValue;
case 'status':
return StatusValue;
case 'date':
return DateValue;
case 'text':
case 'string':
default:

View File

@ -18,6 +18,7 @@ export interface StatusCodeListInterface {
interface RenderStatusLabelOptionsInterface {
size?: MantineSize;
hidden?: boolean;
}
/*
@ -121,6 +122,10 @@ export const StatusRenderer = ({
}) => {
const statusCodes = getStatusCodes(type);
if (options?.hidden) {
return null;
}
if (statusCodes === undefined || statusCodes === null) {
console.warn('StatusRenderer: statusCodes is undefined');
return null;

View File

@ -59,6 +59,7 @@ import {
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState';
import { useUserState } from '../../states/UserState';
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
@ -72,6 +73,13 @@ export default function StockDetail() {
const user = useUserState();
const globalSettings = useGlobalSettingsState();
const enableExpiry = useMemo(
() => globalSettings.isSet('STOCK_ENABLE_EXPIRY'),
[globalSettings]
);
const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false);
@ -214,6 +222,7 @@ export default function StockDetail() {
{
type: 'link',
name: 'parent',
icon: 'sitemap',
label: t`Parent Item`,
model: ModelType.stockitem,
hidden: !stockitem.parent,
@ -258,7 +267,14 @@ export default function StockDetail() {
// Bottom right - any other information
let br: DetailsField[] = [
// TODO: Expiry date
// Expiry date
{
type: 'date',
name: 'expiry_date',
label: t`Expiry Date`,
hidden: !enableExpiry || !stockitem.expiry_date,
icon: 'calendar'
},
// TODO: Ownership
{
type: 'text',
@ -320,7 +336,7 @@ export default function StockDetail() {
<DetailsTable fields={br} item={data} />
</ItemDetailsGrid>
);
}, [stockitem, instanceQuery]);
}, [stockitem, instanceQuery, enableExpiry]);
const showBuildAllocations: boolean = useMemo(() => {
// Determine if "build allocations" should be shown for this stock item
@ -636,6 +652,7 @@ export default function StockDetail() {
/>,
<ActionDropdown
tooltip={t`Stock Operations`}
hidden={!stockitem.in_stock}
icon={<IconPackages />}
actions={[
{
@ -749,7 +766,11 @@ export default function StockDetail() {
<DetailsBadge
color="yellow"
label={t`Available` + `: ${available}`}
visible={!stockitem.serial && available != stockitem.quantity}
visible={
stockitem.in_stock &&
!stockitem.serial &&
available != stockitem.quantity
}
key="available"
/>,
<DetailsBadge
@ -761,11 +782,32 @@ export default function StockDetail() {
<StatusRenderer
status={stockitem.status_custom_key}
type={ModelType.stockitem}
options={{ size: 'lg' }}
options={{
size: 'lg',
hidden: !!stockitem.status_custom_key
}}
key="status"
/>,
<DetailsBadge
color="yellow"
label={t`Stale`}
visible={enableExpiry && stockitem.stale && !stockitem.expired}
key="stale"
/>,
<DetailsBadge
color="orange"
label={t`Expired`}
visible={enableExpiry && stockitem.expired}
key="expired"
/>,
<DetailsBadge
color="red"
label={t`Unavailable`}
visible={stockitem.in_stock == false}
key="unavailable"
/>
];
}, [stockitem, instanceQuery]);
}, [stockitem, instanceQuery, enableExpiry]);
return (
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>

View File

@ -77,18 +77,6 @@ function stockItemTableColumns(): TableColumn[] {
let extra: ReactNode[] = [];
let color = undefined;
// Determine if a stock item is "in stock"
// TODO: Refactor this out into a function
let in_stock =
!record?.belongs_to &&
!record?.consumed_by &&
!record?.customer &&
!record?.is_building &&
!record?.sales_order &&
!record?.expired &&
record?.quantity &&
record?.quantity > 0;
if (record.serial && quantity == 1) {
text = `# ${record.serial}`;
}
@ -101,42 +89,41 @@ function stockItemTableColumns(): TableColumn[] {
size="sm"
>{t`This stock item is in production`}</Text>
);
}
if (record.sales_order) {
} else 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) {
} else 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) {
} else 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) {
} else if (record.consumed_by) {
extra.push(
<Text
key="consumed-by"
size="sm"
>{t`This stock item has been consumed by a build order`}</Text>
);
} else if (!record.in_stock) {
extra.push(
<Text
key="unavailable"
size="sm"
>{t`This stock item is unavailable`}</Text>
);
}
if (record.expired) {
@ -152,53 +139,55 @@ function stockItemTableColumns(): TableColumn[] {
);
}
if (allocated > 0) {
if (allocated >= quantity) {
color = 'orange';
if (record.in_stock) {
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" c="orange">
{t`Available` + `: ${available}`}
</Text>
);
} else {
extra.push(
<Text
key="no-stock"
size="sm"
c="red"
>{t`No stock available`}</Text>
);
}
}
if (quantity <= 0) {
extra.push(
<Text
key="fully-allocated"
key="depleted"
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>
>{t`This stock item has been depleted`}</Text>
);
}
}
if (available != quantity) {
if (available > 0) {
extra.push(
<Text key="available" size="sm" c="orange">
{t`Available` + `: ${available}`}
</Text>
);
} else {
extra.push(
<Text
key="no-stock"
size="sm"
c="red"
>{t`No stock available`}</Text>
);
}
}
if (quantity <= 0) {
extra.push(
<Text
key="depleted"
size="sm"
>{t`This stock item has been depleted`}</Text>
);
}
if (!in_stock) {
if (!record.in_stock) {
color = 'red';
}