diff --git a/src/frontend/src/components/tables/PartStockCell.tsx b/src/frontend/src/components/tables/PartStockCell.tsx
new file mode 100644
index 0000000000..d5e8dc6246
--- /dev/null
+++ b/src/frontend/src/components/tables/PartStockCell.tsx
@@ -0,0 +1,132 @@
+import { t } from '@lingui/core/macro';
+import { Group, Text } from '@mantine/core';
+import type { ReactNode } from 'react';
+import { formatDecimal } from '../../defaults/formatters';
+import { TableHoverCard } from './TableHoverCard';
+
+export function renderPartStockCell(record: any): ReactNode {
+ if (record.virtual) {
+ return (
+
+ {t`Virtual part`}
+
+ );
+ }
+
+ const extra: ReactNode[] = [];
+
+ const stock = record?.total_in_stock ?? 0;
+ const allocated =
+ (record?.allocated_to_build_orders ?? 0) +
+ (record?.allocated_to_sales_orders ?? 0);
+ const available = Math.max(0, stock - allocated);
+ const min_stock = record?.minimum_stock ?? 0;
+ const max_stock = record?.maximum_stock ?? 0;
+
+ let text = String(formatDecimal(stock));
+
+ let color: string | undefined = undefined;
+
+ if (min_stock > stock) {
+ extra.push(
+
+ {`${t`Minimum stock`}: ${formatDecimal(min_stock)}`}
+
+ );
+
+ color = 'orange';
+ }
+
+ if (max_stock > 0 && stock > max_stock) {
+ extra.push(
+
+ {`${t`Maximum stock`}: ${formatDecimal(max_stock)}`}
+
+ );
+ }
+
+ if (record.ordering > 0) {
+ extra.push(
+ {`${t`On Order`}: ${formatDecimal(record.ordering)}`}
+ );
+ }
+
+ if (record.building) {
+ extra.push(
+ {`${t`Building`}: ${formatDecimal(record.building)}`}
+ );
+ }
+
+ if (record.allocated_to_build_orders > 0) {
+ extra.push(
+
+ {`${t`Build Order Allocations`}: ${formatDecimal(record.allocated_to_build_orders)}`}
+
+ );
+ }
+
+ if (record.allocated_to_sales_orders > 0) {
+ extra.push(
+
+ {`${t`Sales Order Allocations`}: ${formatDecimal(record.allocated_to_sales_orders)}`}
+
+ );
+ }
+
+ if (available != stock) {
+ extra.push(
+
+ {t`Available`}: {formatDecimal(available)}
+
+ );
+ }
+
+ if (record.external_stock > 0) {
+ extra.push(
+
+ {t`External stock`}: {formatDecimal(record.external_stock)}
+
+ );
+ }
+
+ if ((record.variant_stock ?? 0) > 0) {
+ extra.push(
+
+ {t`Includes variant stock`}: {formatDecimal(record.variant_stock)}
+
+ );
+ extra.push(
+
+ {t`Direct stock`}: {formatDecimal(record.in_stock ?? 0)}
+
+ );
+ }
+
+ if (stock <= 0) {
+ color = 'red';
+ text = t`No stock`;
+ } else if (available <= 0) {
+ color = 'orange';
+ } else if (available < min_stock) {
+ color = 'yellow';
+ }
+
+ return (
+
+
+ {text}
+
+ {record.units && (
+
+ [{record.units}]
+
+ )}
+
+ }
+ title={t`Stock Information`}
+ extra={extra}
+ />
+ );
+}
diff --git a/src/frontend/src/tables/part/ParametricPartTable.tsx b/src/frontend/src/tables/part/ParametricPartTable.tsx
index 0d30102e1b..32477cc389 100644
--- a/src/frontend/src/tables/part/ParametricPartTable.tsx
+++ b/src/frontend/src/tables/part/ParametricPartTable.tsx
@@ -7,6 +7,7 @@ import {
DescriptionColumn,
PartColumn
} from '../../components/tables/ColumnRenderers';
+import { renderPartStockCell } from '../../components/tables/PartStockCell';
import ParametricDataTable from '../general/ParametricDataTable';
import { PartTableFilters } from './PartTableFilters';
@@ -33,7 +34,8 @@ export default function ParametricPartTable({
},
{
accessor: 'total_in_stock',
- sortable: true
+ sortable: true,
+ render: renderPartStockCell
}
];
}, []);
diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx
index 3122a94dcb..5cf56f21e3 100644
--- a/src/frontend/src/tables/part/PartTable.tsx
+++ b/src/frontend/src/tables/part/PartTable.tsx
@@ -12,14 +12,13 @@ import type { ApiFormFieldSet } from '@lib/types/Forms';
import type { TableColumn } from '@lib/types/Tables';
import type { InvenTreeTableProps } from '@lib/types/Tables';
import { t } from '@lingui/core/macro';
-import { Group, Text } from '@mantine/core';
import {
IconFileUpload,
IconPackageImport,
IconPlus,
IconShoppingCart
} from '@tabler/icons-react';
-import { type ReactNode, useCallback, useMemo, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import {
BooleanColumn,
@@ -31,10 +30,10 @@ import {
PartColumn
} from '../../components/tables/ColumnRenderers';
import { InvenTreeTable } from '../../components/tables/InvenTreeTable';
-import { TableHoverCard } from '../../components/tables/TableHoverCard';
+import { renderPartStockCell } from '../../components/tables/PartStockCell';
import ImportPartWizard from '../../components/wizards/ImportPartWizard';
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
-import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
+import { formatPriceRange } from '../../defaults/formatters';
import { DuplicateField } from '../../forms/CommonFields';
import { dataImporterSessionFields } from '../../forms/ImporterForms';
import { usePartFields } from '../../forms/PartForms';
@@ -84,119 +83,7 @@ function partTableColumns(): TableColumn[] {
accessor: 'total_in_stock',
sortable: true,
filter: ['has_stock', 'low_stock', 'high_stock'],
- render: (record) => {
- if (record.virtual) {
- return (
-
- {t`Virtual part`}
-
- );
- }
-
- const extra: ReactNode[] = [];
-
- const stock = record?.total_in_stock ?? 0;
- const allocated =
- (record?.allocated_to_build_orders ?? 0) +
- (record?.allocated_to_sales_orders ?? 0);
- const available = Math.max(0, stock - allocated);
- const min_stock = record?.minimum_stock ?? 0;
- const max_stock = record?.maximum_stock ?? 0;
-
- let text = String(formatDecimal(stock));
-
- let color: string | undefined = undefined;
-
- if (min_stock > stock) {
- extra.push(
-
- {`${t`Minimum stock`}: ${formatDecimal(min_stock)}`}
-
- );
-
- color = 'orange';
- }
-
- if (max_stock > 0 && stock > max_stock) {
- extra.push(
-
- {`${t`Maximum stock`}: ${formatDecimal(max_stock)}`}
-
- );
- }
-
- if (record.ordering > 0) {
- extra.push(
- {`${t`On Order`}: ${formatDecimal(record.ordering)}`}
- );
- }
-
- if (record.building) {
- extra.push(
- {`${t`Building`}: ${formatDecimal(record.building)}`}
- );
- }
-
- if (record.allocated_to_build_orders > 0) {
- extra.push(
-
- {`${t`Build Order Allocations`}: ${formatDecimal(record.allocated_to_build_orders)}`}
-
- );
- }
-
- if (record.allocated_to_sales_orders > 0) {
- extra.push(
-
- {`${t`Sales Order Allocations`}: ${formatDecimal(record.allocated_to_sales_orders)}`}
-
- );
- }
-
- if (available != stock) {
- extra.push(
-
- {t`Available`}: {formatDecimal(available)}
-
- );
- }
-
- if (record.external_stock > 0) {
- extra.push(
-
- {t`External stock`}: {formatDecimal(record.external_stock)}
-
- );
- }
-
- if (stock <= 0) {
- color = 'red';
- text = t`No stock`;
- } else if (available <= 0) {
- color = 'orange';
- } else if (available < min_stock) {
- color = 'yellow';
- }
-
- return (
-
-
- {text}
-
- {record.units && (
-
- [{record.units}]
-
- )}
-
- }
- title={t`Stock Information`}
- extra={extra}
- />
- );
- }
+ render: renderPartStockCell
},
{
accessor: 'price_range',