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:
parent
48bb5fd579
commit
f77c8a5b5b
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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}>
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user