[feature] Consumable part (#12295)

* Add "consumable" field to the Part model

* Add frontend integration

* Add badge

* Add docs

* Updated docs

* Add API filtering for new field

* Adjust API filters

* Update backend logic

* Adjust frontend

* Additional unit tests

* Update API version

* update test

* Add consumable icon

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
Oliver
2026-07-06 21:12:24 +10:00
committed by GitHub
co-authored by Matthias Mair
parent 47ed86ef9a
commit e6a12564f3
23 changed files with 365 additions and 21 deletions
+3
View File
@@ -103,6 +103,9 @@ export function usePartFields({
setVirtual(value);
}
},
consumable: {
default: false
},
locked: {},
active: {},
starred: {
+2
View File
@@ -83,6 +83,7 @@ import {
type IconProps,
IconQrcode,
IconQuestionMark,
IconRecycle,
IconRefresh,
IconRulerMeasure,
IconSearch,
@@ -207,6 +208,7 @@ const icons: InvenTreeIconType = {
purchaseable: IconShoppingCart,
saleable: IconCurrencyDollar,
virtual: IconWorldCode,
consumable: IconRecycle,
inactive: IconX,
part: IconBox,
supplier_part: IconPackageImport,
+12 -1
View File
@@ -549,6 +549,11 @@ export default function PartDetail() {
name: 'virtual',
label: t`Virtual Part`
},
{
type: 'boolean',
name: 'consumable',
label: t`Consumable Part`
},
{
type: 'boolean',
name: 'starred',
@@ -1002,10 +1007,16 @@ export default function PartDetail() {
key='inactive'
/>,
<DetailsBadge
label={t`Virtual Part`}
label={t`Virtual`}
color='cyan.4'
visible={part.virtual}
key='virtual'
/>,
<DetailsBadge
label={t`Consumable`}
color='cyan.4'
visible={part.consumable}
key='consumable'
/>
];
}, [partRequirements, partRequirementsQuery.isFetching, part]);
@@ -1,6 +1,7 @@
import { ActionButton } from '@lib/components/ActionButton';
import { ProgressBar } from '@lib/components/ProgressBar';
import { RowEditAction, RowViewAction } from '@lib/components/RowActions';
import { YesNoButton } from '@lib/components/YesNoButton';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
@@ -10,7 +11,7 @@ import useTable from '@lib/hooks/UseTable';
import type { TableFilter } from '@lib/types/Filters';
import type { RowAction, TableColumn } from '@lib/types/Tables';
import { t } from '@lingui/core/macro';
import { Alert, Group, Paper, Text } from '@mantine/core';
import { Alert, Center, Group, Paper, Text } from '@mantine/core';
import {
IconArrowRight,
IconCircleCheck,
@@ -54,6 +55,16 @@ import { InvenTreeTable } from '../InvenTreeTable';
import RowExpansionIcon from '../RowExpansionIcon';
import { TableHoverCard } from '../TableHoverCard';
/**
* Return true if the given build line record is "effectively consumable" -
* i.e. either the BOM line itself, or the underlying part, is marked as consumable.
*/
function isLineConsumable(record: any): boolean {
return (
!!record?.bom_item_detail?.consumable || !!record?.part_detail?.consumable
);
}
/**
* Render a sub-table of allocated stock against a particular build line.
*
@@ -366,7 +377,12 @@ export default function BuildLineTable({
ordering: 'consumable',
filter: 'consumable',
hidden: hasOutput,
defaultVisible: false
defaultVisible: false,
render: (record: any) => (
<Center>
<YesNoButton value={isLineConsumable(record)} />
</Center>
)
}),
BooleanColumn({
accessor: 'bom_item_detail.allow_variants',
@@ -499,7 +515,7 @@ export default function BuildLineTable({
hidden: !isActive,
minWidth: 125,
render: (record: any) => {
if (record?.bom_item_detail?.consumable) {
if (isLineConsumable(record)) {
return (
<Text
size='sm'
@@ -545,7 +561,7 @@ export default function BuildLineTable({
hidden: !!output?.pk,
minWidth: 125,
render: (record: any) => {
return record?.bom_item_detail?.consumable ? (
return isLineConsumable(record) ? (
<Text
size='sm'
style={{ fontStyle: 'italic' }}
@@ -747,7 +763,7 @@ export default function BuildLineTable({
(record: any): RowAction[] => {
const part = record.part_detail ?? {};
const in_production = build.status == buildStatus.PRODUCTION;
const consumable: boolean = record.bom_item_detail?.consumable ?? false;
const consumable: boolean = isLineConsumable(record);
const trackable: boolean = part?.trackable ?? false;
const hasOutput: boolean = !!output?.pk;
@@ -907,7 +923,7 @@ export default function BuildLineTable({
onClick={() => {
let rows = table.selectedRecords
.filter((r) => r.allocatedQuantity < r.requiredQuantity)
.filter((r) => !r.bom_item_detail?.consumable);
.filter((r) => !isLineConsumable(r));
if (hasOutput) {
rows = rows.filter((r) => r.trackable);
@@ -216,6 +216,10 @@ function partTableColumns(): TableColumn[] {
accessor: 'virtual',
defaultVisible: false
}),
BooleanColumn({
accessor: 'consumable',
defaultVisible: false
}),
LinkColumn({})
];
}
@@ -104,6 +104,12 @@ export function PartTableFilters(): TableFilter[] {
description: t`Filter by parts which are virtual`,
type: 'boolean'
},
{
name: 'consumable',
label: t`Consumable`,
description: t`Filter by parts which are consumable`,
type: 'boolean'
},
{
name: 'is_template',
label: t`Is Template`,