2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 136 additions and 78 deletions

View File

@ -1,13 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 267
INVENTREE_API_VERSION = 268
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
268 - 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274
- Adds "in_stock" attribute to the StockItem serializer
267 - 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250
- Remove "allocations" field from the SalesOrderShipment API endpoint(s)
- Add "allocated_items" field to the SalesOrderShipment API endpoint(s)

View File

@ -441,6 +441,7 @@ class StockItem(
tags = TaggableManager(blank=True)
# A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock"
# See also: StockItem.in_stock() method
IN_STOCK_FILTER = Q(
quantity__gt=0,
sales_order=None,
@ -1404,16 +1405,20 @@ class StockItem(
return self.children.count()
@property
def in_stock(self):
def in_stock(self) -> bool:
"""Returns True if this item is in stock.
See also: IN_STOCK_FILTER
See also: StockItem.IN_STOCK_FILTER for the db optimized version of this check.
"""
query = StockItem.objects.filter(pk=self.pk)
query = query.filter(StockItem.IN_STOCK_FILTER)
return query.exists()
return all([
self.quantity > 0, # Quantity must be greater than zero
self.sales_order is None, # Not assigned to a SalesOrder
self.belongs_to is None, # Not installed inside another StockItem
self.customer is None, # Not assigned to a customer
self.consumed_by is None, # Not consumed by a build
not self.is_building, # Not part of an active build
self.status in StockStatusGroups.AVAILABLE_CODES, # Status is "available"
])
@property
def can_adjust_location(self):

View File

@ -358,6 +358,7 @@ class StockItemSerializer(
'customer',
'delete_on_deplete',
'expiry_date',
'in_stock',
'is_building',
'link',
'location',
@ -468,6 +469,8 @@ class StockItemSerializer(
child=serializers.DictField(), source='location.get_path', read_only=True
)
in_stock = serializers.BooleanField(read_only=True, label=_('In Stock'))
"""
Field used when creating a stock item
"""
@ -519,6 +522,10 @@ class StockItemSerializer(
'supplier_part__manufacturer_part__manufacturer',
'supplier_part__tags',
'test_results',
'customer',
'belongs_to',
'sales_order',
'consumed_by',
'tags',
)

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';
}