mirror of
https://github.com/inventree/InvenTree.git
synced 2026-02-13 09:47:09 +00:00
Display more output information in "build allocated stock" table (#11276)
* Add "install_into_detail" to BuildItem serializer * Enhance the "resolveItem" function * Add "StockColumn" renderer * Fix output column for BuildAllocatedStockTable * Replace column in stock item table * More column refactoring * Bump API version * Add InvenTreeOutputOption descriptions * Prefetch for better API performance * Updated playwright testing
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 451
|
INVENTREE_API_VERSION = 452
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v452 -> 2026-02-10 : https://github.com/inventree/InvenTree/pull/11276
|
||||||
|
- Adds "install_into_detail" field to the BuildItem API endpoint
|
||||||
|
|
||||||
v451 -> 2026-02-10 : https://github.com/inventree/InvenTree/pull/11277
|
v451 -> 2026-02-10 : https://github.com/inventree/InvenTree/pull/11277
|
||||||
- Adds sorting to multiple part related endpoints (part, IPN, ...)
|
- Adds sorting to multiple part related endpoints (part, IPN, ...)
|
||||||
|
|
||||||
|
|||||||
@@ -808,13 +808,17 @@ class BuildCancel(BuildOrderContextMixin, CreateAPI):
|
|||||||
serializer_class = build.serializers.BuildCancelSerializer
|
serializer_class = build.serializers.BuildCancelSerializer
|
||||||
|
|
||||||
|
|
||||||
class BuildItemDetail(RetrieveUpdateDestroyAPI):
|
class BuildItemMixin:
|
||||||
"""API endpoint for detail view of a BuildItem object."""
|
"""Mixin class for BuildItem API endpoints."""
|
||||||
|
|
||||||
queryset = BuildItem.objects.all()
|
queryset = BuildItem.objects.all().prefetch_related('stock_item__location')
|
||||||
serializer_class = build.serializers.BuildItemSerializer
|
serializer_class = build.serializers.BuildItemSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class BuildItemDetail(BuildItemMixin, RetrieveUpdateDestroyAPI):
|
||||||
|
"""API endpoint for detail view of a BuildItem object."""
|
||||||
|
|
||||||
|
|
||||||
class BuildItemFilter(FilterSet):
|
class BuildItemFilter(FilterSet):
|
||||||
"""Custom filterset for the BuildItemList API endpoint."""
|
"""Custom filterset for the BuildItemList API endpoint."""
|
||||||
|
|
||||||
@@ -899,16 +903,45 @@ class BuildItemOutputOptions(OutputConfiguration):
|
|||||||
"""Output options for BuildItem endpoint."""
|
"""Output options for BuildItem endpoint."""
|
||||||
|
|
||||||
OPTIONS = [
|
OPTIONS = [
|
||||||
InvenTreeOutputOption('part_detail'),
|
InvenTreeOutputOption(
|
||||||
InvenTreeOutputOption('location_detail'),
|
'part_detail',
|
||||||
InvenTreeOutputOption('stock_detail'),
|
default=False,
|
||||||
InvenTreeOutputOption('build_detail'),
|
description='Include detailed information about the part associated with this build item.',
|
||||||
InvenTreeOutputOption('supplier_part_detail'),
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'location_detail',
|
||||||
|
default=False,
|
||||||
|
description='Include detailed information about the location of the allocated stock item.',
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'stock_detail',
|
||||||
|
default=False,
|
||||||
|
description='Include detailed information about the allocated stock item.',
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'build_detail',
|
||||||
|
default=False,
|
||||||
|
description='Include detailed information about the associated build order.',
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'supplier_part_detail',
|
||||||
|
default=False,
|
||||||
|
description='Include detailed information about the supplier part associated with this build item.',
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'install_into_detail',
|
||||||
|
default=False,
|
||||||
|
description='Include detailed information about the build output for this build item.',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BuildItemList(
|
class BuildItemList(
|
||||||
DataExportViewMixin, OutputOptionsMixin, BulkDeleteMixin, ListCreateAPI
|
BuildItemMixin,
|
||||||
|
DataExportViewMixin,
|
||||||
|
OutputOptionsMixin,
|
||||||
|
BulkDeleteMixin,
|
||||||
|
ListCreateAPI,
|
||||||
):
|
):
|
||||||
"""API endpoint for accessing a list of BuildItem objects.
|
"""API endpoint for accessing a list of BuildItem objects.
|
||||||
|
|
||||||
@@ -917,8 +950,6 @@ class BuildItemList(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
output_options = BuildItemOutputOptions
|
output_options = BuildItemOutputOptions
|
||||||
queryset = BuildItem.objects.all()
|
|
||||||
serializer_class = build.serializers.BuildItemSerializer
|
|
||||||
filterset_class = BuildItemFilter
|
filterset_class = BuildItemFilter
|
||||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||||
|
|
||||||
|
|||||||
@@ -1179,6 +1179,7 @@ class BuildItemSerializer(
|
|||||||
'part_detail',
|
'part_detail',
|
||||||
'stock_item_detail',
|
'stock_item_detail',
|
||||||
'supplier_part_detail',
|
'supplier_part_detail',
|
||||||
|
'install_into_detail',
|
||||||
# The following fields are only used for data export
|
# The following fields are only used for data export
|
||||||
'bom_reference',
|
'bom_reference',
|
||||||
'bom_part_id',
|
'bom_part_id',
|
||||||
@@ -1244,6 +1245,21 @@ class BuildItemSerializer(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
install_into_detail = enable_filter(
|
||||||
|
StockItemSerializer(
|
||||||
|
source='install_into',
|
||||||
|
read_only=True,
|
||||||
|
allow_null=True,
|
||||||
|
label=_('Install Into'),
|
||||||
|
part_detail=False,
|
||||||
|
location_detail=False,
|
||||||
|
supplier_part_detail=False,
|
||||||
|
path_detail=False,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
prefetch_fields=['install_into', 'install_into__part'],
|
||||||
|
)
|
||||||
|
|
||||||
location = serializers.PrimaryKeyRelatedField(
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
label=_('Location'), source='stock_item.location', many=False, read_only=True
|
label=_('Location'), source='stock_item.location', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ export function isTrue(value: any): boolean {
|
|||||||
* Allows for retrieval of nested items in an object.
|
* Allows for retrieval of nested items in an object.
|
||||||
*/
|
*/
|
||||||
export function resolveItem(obj: any, path: string): any {
|
export function resolveItem(obj: any, path: string): any {
|
||||||
|
// Return the top-level object if no path is provided
|
||||||
|
if (path == null || path === '') {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
const properties = path.split('.');
|
const properties = path.split('.');
|
||||||
return properties.reduce((prev, curr) => prev?.[curr], obj);
|
return properties.reduce((prev, curr) => prev?.[curr], obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,10 +93,10 @@ export function PartColumn(props: PartColumnProps): TableColumn {
|
|||||||
switchable: false,
|
switchable: false,
|
||||||
minWidth: '175px',
|
minWidth: '175px',
|
||||||
render: (record: any) => {
|
render: (record: any) => {
|
||||||
const part =
|
const part = resolveItem(
|
||||||
props.part === ''
|
record,
|
||||||
? record
|
props.part ?? props.accessor ?? 'part_detail'
|
||||||
: resolveItem(record, props.part ?? props.accessor ?? 'part_detail');
|
);
|
||||||
|
|
||||||
return RenderPartColumn({
|
return RenderPartColumn({
|
||||||
part: part,
|
part: part,
|
||||||
@@ -107,6 +107,174 @@ export function PartColumn(props: PartColumnProps): TableColumn {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StockColumnProps = TableColumnProps & {
|
||||||
|
nullMessage?: string | ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render a StockItem instance within a table
|
||||||
|
export function StockColumn(props: StockColumnProps): TableColumn {
|
||||||
|
return {
|
||||||
|
accessor: props.accessor ?? 'stock_item',
|
||||||
|
title: t`Stock Item`,
|
||||||
|
...props,
|
||||||
|
render: (record: any) => {
|
||||||
|
const stock_item =
|
||||||
|
resolveItem(record, props.accessor ?? 'stock_item_detail') ?? {};
|
||||||
|
const part = stock_item.part_detail ?? {};
|
||||||
|
|
||||||
|
const quantity = stock_item.quantity ?? 0;
|
||||||
|
const allocated = stock_item.allocated ?? 0;
|
||||||
|
const available = quantity - allocated;
|
||||||
|
|
||||||
|
const extra: ReactNode[] = [];
|
||||||
|
let color = undefined;
|
||||||
|
let text = formatDecimal(quantity);
|
||||||
|
|
||||||
|
// Handle case where stock item detail is not provided
|
||||||
|
if (!stock_item || !stock_item.pk) {
|
||||||
|
return props.nullMessage ?? '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override with serial number if available
|
||||||
|
if (stock_item.serial && quantity == 1) {
|
||||||
|
text = `# ${stock_item.serial}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.is_building) {
|
||||||
|
color = 'blue';
|
||||||
|
extra.push(
|
||||||
|
<Text
|
||||||
|
key='production'
|
||||||
|
size='sm'
|
||||||
|
>{t`This stock item is in production`}</Text>
|
||||||
|
);
|
||||||
|
} 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>
|
||||||
|
);
|
||||||
|
} else if (record.customer) {
|
||||||
|
extra.push(
|
||||||
|
<Text
|
||||||
|
key='customer'
|
||||||
|
size='sm'
|
||||||
|
>{t`This stock item has been assigned to a customer`}</Text>
|
||||||
|
);
|
||||||
|
} else if (record.belongs_to) {
|
||||||
|
extra.push(
|
||||||
|
<Text
|
||||||
|
key='belongs-to'
|
||||||
|
size='sm'
|
||||||
|
>{t`This stock item is installed in another stock item`}</Text>
|
||||||
|
);
|
||||||
|
} 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) {
|
||||||
|
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 (record.in_stock) {
|
||||||
|
if (allocated > 0) {
|
||||||
|
if (allocated > quantity) {
|
||||||
|
color = 'red';
|
||||||
|
extra.push(
|
||||||
|
<Text
|
||||||
|
key='over-allocated'
|
||||||
|
size='sm'
|
||||||
|
>{t`This stock item is over-allocated`}</Text>
|
||||||
|
);
|
||||||
|
} else 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`}: ${formatDecimal(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 (!record.in_stock) {
|
||||||
|
color = 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHoverCard
|
||||||
|
value={
|
||||||
|
<Group gap='xs' justify='left' wrap='nowrap'>
|
||||||
|
<Text c={color}>{text}</Text>
|
||||||
|
{part.units && (
|
||||||
|
<Text size='xs' c={color}>
|
||||||
|
[{part.units}]
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
title={t`Stock Information`}
|
||||||
|
extra={extra}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function CompanyColumn({
|
export function CompanyColumn({
|
||||||
company
|
company
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import {
|
|||||||
LocationColumn,
|
LocationColumn,
|
||||||
PartColumn,
|
PartColumn,
|
||||||
ReferenceColumn,
|
ReferenceColumn,
|
||||||
StatusColumn
|
StatusColumn,
|
||||||
|
StockColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import { IncludeVariantsFilter, StockLocationFilter } from '../Filter';
|
import { IncludeVariantsFilter, StockLocationFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
@@ -142,11 +143,11 @@ export default function BuildAllocatedStockTable({
|
|||||||
switchable: true,
|
switchable: true,
|
||||||
sortable: true
|
sortable: true
|
||||||
}),
|
}),
|
||||||
{
|
StockColumn({
|
||||||
accessor: 'install_into',
|
accessor: 'install_into_detail',
|
||||||
title: t`Build Output`,
|
title: t`Build Output`,
|
||||||
sortable: true
|
sortable: false
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
accessor: 'sku',
|
accessor: 'sku',
|
||||||
title: t`Supplier Part`,
|
title: t`Supplier Part`,
|
||||||
@@ -307,6 +308,7 @@ export default function BuildAllocatedStockTable({
|
|||||||
part_detail: showPartInfo ?? false,
|
part_detail: showPartInfo ?? false,
|
||||||
location_detail: true,
|
location_detail: true,
|
||||||
stock_detail: true,
|
stock_detail: true,
|
||||||
|
install_into_detail: true,
|
||||||
supplier_detail: true
|
supplier_detail: true
|
||||||
},
|
},
|
||||||
enableBulkDelete: allowEdit && user.hasDeleteRole(UserRoles.build),
|
enableBulkDelete: allowEdit && user.hasDeleteRole(UserRoles.build),
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ import {
|
|||||||
PartColumn,
|
PartColumn,
|
||||||
ProjectCodeColumn,
|
ProjectCodeColumn,
|
||||||
ReferenceColumn,
|
ReferenceColumn,
|
||||||
StatusColumn
|
StatusColumn,
|
||||||
|
StockColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import { StatusFilterOptions } from '../Filter';
|
import { StatusFilterOptions } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
@@ -121,20 +122,12 @@ export default function ReturnOrderLineItemTable({
|
|||||||
DescriptionColumn({
|
DescriptionColumn({
|
||||||
accessor: 'part_detail.description'
|
accessor: 'part_detail.description'
|
||||||
}),
|
}),
|
||||||
{
|
StockColumn({
|
||||||
accessor: 'item_detail.serial',
|
accessor: 'item_detail',
|
||||||
title: t`Quantity`,
|
|
||||||
switchable: false,
|
switchable: false,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
ordering: 'stock',
|
ordering: 'stock'
|
||||||
render: (record: any) => {
|
}),
|
||||||
if (record.item_detail.serial && record.quantity == 1) {
|
|
||||||
return `# ${record.item_detail.serial}`;
|
|
||||||
} else {
|
|
||||||
return record.quantity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
StatusColumn({
|
StatusColumn({
|
||||||
model: ModelType.stockitem,
|
model: ModelType.stockitem,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { formatDecimal } from '@lib/functions/Formatting';
|
|
||||||
import type { TableColumn } from '@lib/types/Tables';
|
import type { TableColumn } from '@lib/types/Tables';
|
||||||
import {
|
import {
|
||||||
useStockItemInstallFields,
|
useStockItemInstallFields,
|
||||||
@@ -17,7 +16,7 @@ import {
|
|||||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { PartColumn, StatusColumn } from '../ColumnRenderers';
|
import { PartColumn, StatusColumn, StockColumn } from '../ColumnRenderers';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
export default function InstalledItemsTable({
|
export default function InstalledItemsTable({
|
||||||
@@ -62,19 +61,11 @@ export default function InstalledItemsTable({
|
|||||||
PartColumn({
|
PartColumn({
|
||||||
part: 'part_detail'
|
part: 'part_detail'
|
||||||
}),
|
}),
|
||||||
{
|
StockColumn({
|
||||||
accessor: 'quantity',
|
accessor: '',
|
||||||
switchable: false,
|
title: t`Stock Item`,
|
||||||
render: (record: any) => {
|
sortable: false
|
||||||
let text = formatDecimal(record.quantity);
|
}),
|
||||||
|
|
||||||
if (record.serial && record.quantity == 1) {
|
|
||||||
text = `# ${record.serial}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessor: 'batch',
|
accessor: 'batch',
|
||||||
switchable: false
|
switchable: false
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Group, Text } from '@mantine/core';
|
import { useMemo, useState } from 'react';
|
||||||
import { type ReactNode, useMemo, useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { ActionButton } from '@lib/components/ActionButton';
|
import { ActionButton } from '@lib/components/ActionButton';
|
||||||
@@ -14,11 +13,7 @@ import type { TableFilter } from '@lib/types/Filters';
|
|||||||
import type { StockOperationProps } from '@lib/types/Forms';
|
import type { StockOperationProps } from '@lib/types/Forms';
|
||||||
import type { TableColumn } from '@lib/types/Tables';
|
import type { TableColumn } from '@lib/types/Tables';
|
||||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||||
import {
|
import { formatCurrency, formatPriceRange } from '../../defaults/formatters';
|
||||||
formatCurrency,
|
|
||||||
formatDecimal,
|
|
||||||
formatPriceRange
|
|
||||||
} from '../../defaults/formatters';
|
|
||||||
import { useStockFields } from '../../forms/StockForms';
|
import { useStockFields } from '../../forms/StockForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||||
@@ -31,7 +26,8 @@ import {
|
|||||||
DescriptionColumn,
|
DescriptionColumn,
|
||||||
LocationColumn,
|
LocationColumn,
|
||||||
PartColumn,
|
PartColumn,
|
||||||
StatusColumn
|
StatusColumn,
|
||||||
|
StockColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import {
|
import {
|
||||||
BatchFilter,
|
BatchFilter,
|
||||||
@@ -47,7 +43,6 @@ import {
|
|||||||
SupplierFilter
|
SupplierFilter
|
||||||
} from '../Filter';
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import { TableHoverCard } from '../TableHoverCard';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a list of columns for the stock item table
|
* Construct a list of columns for the stock item table
|
||||||
@@ -79,161 +74,12 @@ function stockItemTableColumns({
|
|||||||
DescriptionColumn({
|
DescriptionColumn({
|
||||||
accessor: 'part_detail.description'
|
accessor: 'part_detail.description'
|
||||||
}),
|
}),
|
||||||
{
|
StockColumn({
|
||||||
accessor: 'quantity',
|
accessor: '',
|
||||||
ordering: 'stock',
|
|
||||||
sortable: true,
|
|
||||||
title: t`Stock`,
|
title: t`Stock`,
|
||||||
render: (record: any) => {
|
sortable: true,
|
||||||
// TODO: Push this out into a custom renderer
|
ordering: 'stock'
|
||||||
const quantity = record?.quantity ?? 0;
|
}),
|
||||||
const allocated = record?.allocated ?? 0;
|
|
||||||
const available = quantity - allocated;
|
|
||||||
let text = formatDecimal(quantity);
|
|
||||||
const part = record?.part_detail ?? {};
|
|
||||||
const 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>
|
|
||||||
);
|
|
||||||
} 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>
|
|
||||||
);
|
|
||||||
} else if (record.customer) {
|
|
||||||
extra.push(
|
|
||||||
<Text
|
|
||||||
key='customer'
|
|
||||||
size='sm'
|
|
||||||
>{t`This stock item has been assigned to a customer`}</Text>
|
|
||||||
);
|
|
||||||
} else if (record.belongs_to) {
|
|
||||||
extra.push(
|
|
||||||
<Text
|
|
||||||
key='belongs-to'
|
|
||||||
size='sm'
|
|
||||||
>{t`This stock item is installed in another stock item`}</Text>
|
|
||||||
);
|
|
||||||
} 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) {
|
|
||||||
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 (record.in_stock) {
|
|
||||||
if (allocated > 0) {
|
|
||||||
if (allocated > quantity) {
|
|
||||||
color = 'red';
|
|
||||||
extra.push(
|
|
||||||
<Text
|
|
||||||
key='over-allocated'
|
|
||||||
size='sm'
|
|
||||||
>{t`This stock item is over-allocated`}</Text>
|
|
||||||
);
|
|
||||||
} else 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`}: ${formatDecimal(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 (!record.in_stock) {
|
|
||||||
color = 'red';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableHoverCard
|
|
||||||
value={
|
|
||||||
<Group gap='xs' justify='left' wrap='nowrap'>
|
|
||||||
<Text c={color}>{text}</Text>
|
|
||||||
{part.units && (
|
|
||||||
<Text size='xs' c={color}>
|
|
||||||
[{part.units}]
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
}
|
|
||||||
title={t`Stock Information`}
|
|
||||||
extra={extra}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
StatusColumn({ model: ModelType.stockitem }),
|
StatusColumn({ model: ModelType.stockitem }),
|
||||||
{
|
{
|
||||||
accessor: 'batch',
|
accessor: 'batch',
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ import {
|
|||||||
} from '../../components/render/Stock';
|
} from '../../components/render/Stock';
|
||||||
import { RenderUser } from '../../components/render/User';
|
import { RenderUser } from '../../components/render/User';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { DateColumn, DescriptionColumn, PartColumn } from '../ColumnRenderers';
|
import {
|
||||||
|
DateColumn,
|
||||||
|
DescriptionColumn,
|
||||||
|
PartColumn,
|
||||||
|
StockColumn
|
||||||
|
} from '../ColumnRenderers';
|
||||||
import {
|
import {
|
||||||
IncludeVariantsFilter,
|
IncludeVariantsFilter,
|
||||||
MaxDateFilter,
|
MaxDateFilter,
|
||||||
@@ -241,29 +246,16 @@ export function StockTrackingTable({
|
|||||||
switchable: true,
|
switchable: true,
|
||||||
hidden: !partId
|
hidden: !partId
|
||||||
},
|
},
|
||||||
{
|
StockColumn({
|
||||||
accessor: 'item',
|
|
||||||
title: t`Stock Item`,
|
title: t`Stock Item`,
|
||||||
|
accessor: 'item_detail',
|
||||||
|
nullMessage: (
|
||||||
|
<Text size='sm' c='red'>{t`Stock item no longer exists`}</Text>
|
||||||
|
),
|
||||||
sortable: false,
|
sortable: false,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
hidden: !partId,
|
hidden: !partId
|
||||||
render: (record: any) => {
|
}),
|
||||||
const item = record.item_detail;
|
|
||||||
if (!item) {
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
c='red'
|
|
||||||
size='xs'
|
|
||||||
fs='italic'
|
|
||||||
>{t`Stock item no longer exists`}</Text>
|
|
||||||
);
|
|
||||||
} else if (item.serial && item.quantity == 1) {
|
|
||||||
return `${t`Serial`} #${item.serial}`;
|
|
||||||
} else {
|
|
||||||
return `${t`Item ID`} ${item.pk}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DescriptionColumn({
|
DescriptionColumn({
|
||||||
accessor: 'label'
|
accessor: 'label'
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -470,8 +470,8 @@ test('Stock - Tracking', async ({ browser }) => {
|
|||||||
.getByRole('cell', { name: 'Thumbnail Blue Widget' })
|
.getByRole('cell', { name: 'Thumbnail Blue Widget' })
|
||||||
.first()
|
.first()
|
||||||
.waitFor();
|
.waitFor();
|
||||||
await page.getByRole('cell', { name: 'Item ID 232' }).first().waitFor();
|
|
||||||
await page.getByRole('cell', { name: 'Serial #116' }).first().waitFor();
|
await page.getByText('# 162').first().waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Stock - Location', async ({ browser }) => {
|
test('Stock - Location', async ({ browser }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user