diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index eb268f6a8b..ab2ab9d327 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -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) diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index b4eb7e523c..46c8d30595 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -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): diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 5a6b5d9454..c131d84acc 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -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', ) diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index c1c9ff0849..a851b7af91 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -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) { + return {formatDate(props.field_value?.toString())}; +} + /** * 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: diff --git a/src/frontend/src/components/render/StatusRenderer.tsx b/src/frontend/src/components/render/StatusRenderer.tsx index 680e9c37e1..e9cfb806b2 100644 --- a/src/frontend/src/components/render/StatusRenderer.tsx +++ b/src/frontend/src/components/render/StatusRenderer.tsx @@ -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; diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 68b0b726d2..dbbd3eabd2 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -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() { ); - }, [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() { />,