feat(ui): show variant stock indicator in parts table (#5959) (#12413)

This commit is contained in:
br413
2026-07-20 09:49:50 +10:00
committed by GitHub
parent 4289f13c7f
commit fcbe4302b9
3 changed files with 139 additions and 118 deletions
@@ -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 (
<Text size='sm' c='dimmed' fs='italic'>
{t`Virtual part`}
</Text>
);
}
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(
<Text key='min-stock' c='orange'>
{`${t`Minimum stock`}: ${formatDecimal(min_stock)}`}
</Text>
);
color = 'orange';
}
if (max_stock > 0 && stock > max_stock) {
extra.push(
<Text key='max-stock' c='teal'>
{`${t`Maximum stock`}: ${formatDecimal(max_stock)}`}
</Text>
);
}
if (record.ordering > 0) {
extra.push(
<Text key='on-order'>{`${t`On Order`}: ${formatDecimal(record.ordering)}`}</Text>
);
}
if (record.building) {
extra.push(
<Text key='building'>{`${t`Building`}: ${formatDecimal(record.building)}`}</Text>
);
}
if (record.allocated_to_build_orders > 0) {
extra.push(
<Text key='bo-allocations'>
{`${t`Build Order Allocations`}: ${formatDecimal(record.allocated_to_build_orders)}`}
</Text>
);
}
if (record.allocated_to_sales_orders > 0) {
extra.push(
<Text key='so-allocations'>
{`${t`Sales Order Allocations`}: ${formatDecimal(record.allocated_to_sales_orders)}`}
</Text>
);
}
if (available != stock) {
extra.push(
<Text key='available'>
{t`Available`}: {formatDecimal(available)}
</Text>
);
}
if (record.external_stock > 0) {
extra.push(
<Text key='external'>
{t`External stock`}: {formatDecimal(record.external_stock)}
</Text>
);
}
if ((record.variant_stock ?? 0) > 0) {
extra.push(
<Text key='variant-stock' size='sm'>
{t`Includes variant stock`}: {formatDecimal(record.variant_stock)}
</Text>
);
extra.push(
<Text key='direct-stock' size='sm'>
{t`Direct stock`}: {formatDecimal(record.in_stock ?? 0)}
</Text>
);
}
if (stock <= 0) {
color = 'red';
text = t`No stock`;
} else if (available <= 0) {
color = 'orange';
} else if (available < min_stock) {
color = 'yellow';
}
return (
<TableHoverCard
value={
<Group gap='xs' justify='left' wrap='nowrap'>
<Text c={color} size='sm'>
{text}
</Text>
{record.units && (
<Text size='xs' c={color}>
[{record.units}]
</Text>
)}
</Group>
}
title={t`Stock Information`}
extra={extra}
/>
);
}
@@ -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
}
];
}, []);
+4 -117
View File
@@ -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 (
<Text size='sm' c='dimmed' fs='italic'>
{t`Virtual part`}
</Text>
);
}
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(
<Text key='min-stock' c='orange'>
{`${t`Minimum stock`}: ${formatDecimal(min_stock)}`}
</Text>
);
color = 'orange';
}
if (max_stock > 0 && stock > max_stock) {
extra.push(
<Text key='max-stock' c='teal'>
{`${t`Maximum stock`}: ${formatDecimal(max_stock)}`}
</Text>
);
}
if (record.ordering > 0) {
extra.push(
<Text key='on-order'>{`${t`On Order`}: ${formatDecimal(record.ordering)}`}</Text>
);
}
if (record.building) {
extra.push(
<Text key='building'>{`${t`Building`}: ${formatDecimal(record.building)}`}</Text>
);
}
if (record.allocated_to_build_orders > 0) {
extra.push(
<Text key='bo-allocations'>
{`${t`Build Order Allocations`}: ${formatDecimal(record.allocated_to_build_orders)}`}
</Text>
);
}
if (record.allocated_to_sales_orders > 0) {
extra.push(
<Text key='so-allocations'>
{`${t`Sales Order Allocations`}: ${formatDecimal(record.allocated_to_sales_orders)}`}
</Text>
);
}
if (available != stock) {
extra.push(
<Text key='available'>
{t`Available`}: {formatDecimal(available)}
</Text>
);
}
if (record.external_stock > 0) {
extra.push(
<Text key='external'>
{t`External stock`}: {formatDecimal(record.external_stock)}
</Text>
);
}
if (stock <= 0) {
color = 'red';
text = t`No stock`;
} else if (available <= 0) {
color = 'orange';
} else if (available < min_stock) {
color = 'yellow';
}
return (
<TableHoverCard
value={
<Group gap='xs' justify='left' wrap='nowrap'>
<Text c={color} size='sm'>
{text}
</Text>
{record.units && (
<Text size='xs' c={color}>
[{record.units}]
</Text>
)}
</Group>
}
title={t`Stock Information`}
extra={extra}
/>
);
}
render: renderPartStockCell
},
{
accessor: 'price_range',