Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""InvenTree API version information."""

# 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."""

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
- Adds sorting to multiple part related endpoints (part, IPN, ...)

Expand Down
53 changes: 42 additions & 11 deletions src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,13 +808,17 @@ class BuildCancel(BuildOrderContextMixin, CreateAPI):
serializer_class = build.serializers.BuildCancelSerializer


class BuildItemDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a BuildItem object."""
class BuildItemMixin:
"""Mixin class for BuildItem API endpoints."""

queryset = BuildItem.objects.all()
queryset = BuildItem.objects.all().prefetch_related('stock_item__location')
serializer_class = build.serializers.BuildItemSerializer


class BuildItemDetail(BuildItemMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a BuildItem object."""


class BuildItemFilter(FilterSet):
"""Custom filterset for the BuildItemList API endpoint."""

Expand Down Expand Up @@ -899,16 +903,45 @@ class BuildItemOutputOptions(OutputConfiguration):
"""Output options for BuildItem endpoint."""

OPTIONS = [
InvenTreeOutputOption('part_detail'),
InvenTreeOutputOption('location_detail'),
InvenTreeOutputOption('stock_detail'),
InvenTreeOutputOption('build_detail'),
InvenTreeOutputOption('supplier_part_detail'),
InvenTreeOutputOption(
'part_detail',
default=False,
description='Include detailed information about the part associated with this build item.',
),
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(
DataExportViewMixin, OutputOptionsMixin, BulkDeleteMixin, ListCreateAPI
BuildItemMixin,
DataExportViewMixin,
OutputOptionsMixin,
BulkDeleteMixin,
ListCreateAPI,
):
"""API endpoint for accessing a list of BuildItem objects.

Expand All @@ -917,8 +950,6 @@ class BuildItemList(
"""

output_options = BuildItemOutputOptions
queryset = BuildItem.objects.all()
serializer_class = build.serializers.BuildItemSerializer
filterset_class = BuildItemFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS

Expand Down
16 changes: 16 additions & 0 deletions src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,7 @@ class Meta:
'part_detail',
'stock_item_detail',
'supplier_part_detail',
'install_into_detail',
# The following fields are only used for data export
'bom_reference',
'bom_part_id',
Expand Down Expand Up @@ -1244,6 +1245,21 @@ class Meta:
],
)

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'],
)
Comment on lines +1248 to +1261
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you looked into the performance impact of this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some performance hit by adding an enable_filter - even if it is not used in the final API output.

#11073 would be useful here


location = serializers.PrimaryKeyRelatedField(
label=_('Location'), source='stock_item.location', many=False, read_only=True
)
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/lib/functions/Conversion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export function isTrue(value: any): boolean {
* Allows for retrieval of nested items in an object.
*/
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('.');
return properties.reduce((prev, curr) => prev?.[curr], obj);
}
Expand Down
176 changes: 172 additions & 4 deletions src/frontend/src/tables/ColumnRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ export function PartColumn(props: PartColumnProps): TableColumn {
switchable: false,
minWidth: '175px',
render: (record: any) => {
const part =
props.part === ''
? record
: resolveItem(record, props.part ?? props.accessor ?? 'part_detail');
const part = resolveItem(
record,
props.part ?? props.accessor ?? 'part_detail'
);

return RenderPartColumn({
part: part,
Expand All @@ -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({
company
}: {
Expand Down
12 changes: 7 additions & 5 deletions src/frontend/src/tables/build/BuildAllocatedStockTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
LocationColumn,
PartColumn,
ReferenceColumn,
StatusColumn
StatusColumn,
StockColumn
} from '../ColumnRenderers';
import { IncludeVariantsFilter, StockLocationFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
Expand Down Expand Up @@ -142,11 +143,11 @@ export default function BuildAllocatedStockTable({
switchable: true,
sortable: true
}),
{
accessor: 'install_into',
StockColumn({
accessor: 'install_into_detail',
title: t`Build Output`,
sortable: true
},
sortable: false
}),
{
accessor: 'sku',
title: t`Supplier Part`,
Expand Down Expand Up @@ -307,6 +308,7 @@ export default function BuildAllocatedStockTable({
part_detail: showPartInfo ?? false,
location_detail: true,
stock_detail: true,
install_into_detail: true,
supplier_detail: true
},
enableBulkDelete: allowEdit && user.hasDeleteRole(UserRoles.build),
Expand Down
19 changes: 6 additions & 13 deletions src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ import {
PartColumn,
ProjectCodeColumn,
ReferenceColumn,
StatusColumn
StatusColumn,
StockColumn
} from '../ColumnRenderers';
import { StatusFilterOptions } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
Expand Down Expand Up @@ -121,20 +122,12 @@ export default function ReturnOrderLineItemTable({
DescriptionColumn({
accessor: 'part_detail.description'
}),
{
accessor: 'item_detail.serial',
title: t`Quantity`,
StockColumn({
accessor: 'item_detail',
switchable: false,
sortable: true,
ordering: 'stock',
render: (record: any) => {
if (record.item_detail.serial && record.quantity == 1) {
return `# ${record.item_detail.serial}`;
} else {
return record.quantity;
}
}
},
ordering: 'stock'
}),
StatusColumn({
model: ModelType.stockitem,
sortable: false,
Expand Down
Loading
Loading