Update backend logic

This commit is contained in:
Oliver Walters 2026-07-02 22:54:57 +00:00
parent 4b21f9faa6
commit 99494ba9dc
4 changed files with 54 additions and 12 deletions

View File

@ -974,7 +974,9 @@ class Build(
items_to_delete = []
lines = self.untracked_line_items.all()
lines = lines.exclude(bom_item__consumable=True)
lines = lines.exclude(
part.models.BomItem.consumable_filter(prefix='bom_item__')
)
lines = lines.annotate(allocated=annotate_allocated_quantity())
for build_line in lines:
@ -1254,13 +1256,16 @@ class Build(
return allocations
tracked_line_items = self.tracked_line_items.filter(
bom_item__consumable=False, bom_item__sub_part__virtual=False
part.models.BomItem.consumable_filter(
consumable=False, prefix='bom_item__'
),
bom_item__sub_part__virtual=False,
)
for line_item in tracked_line_items:
bom_item = line_item.bom_item
if bom_item.consumable:
if bom_item.is_consumable:
# Do not auto-allocate stock to consumable BOM items
continue
@ -1384,7 +1389,7 @@ class Build(
# Find the referenced BomItem
bom_item = line_item.bom_item
if bom_item.consumable:
if bom_item.is_consumable:
# Do not auto-allocate stock to consumable BOM items
continue
@ -1504,7 +1509,9 @@ class Build(
lines = self.build_lines.all()
# Remove any 'consumable' line items
lines = lines.exclude(bom_item__consumable=True)
lines = lines.exclude(
part.models.BomItem.consumable_filter(prefix='bom_item__')
)
if tracked is True:
lines = lines.filter(bom_item__sub_part__trackable=True)
@ -1541,7 +1548,9 @@ class Build(
we need to test all "trackable" BuildLine objects
"""
lines = self.build_lines.filter(bom_item__sub_part__trackable=True)
lines = lines.exclude(bom_item__consumable=True)
lines = lines.exclude(
part.models.BomItem.consumable_filter(prefix='bom_item__')
)
# Find any lines which have not been fully allocated
for line in lines:
@ -1565,7 +1574,9 @@ class Build(
Returns:
True if any BuildLine has been over-allocated.
"""
lines = self.build_lines.all().exclude(bom_item__consumable=True)
lines = self.build_lines.all().exclude(
part.models.BomItem.consumable_filter(prefix='bom_item__')
)
lines = lines.prefetch_related('allocations')
@ -1794,7 +1805,7 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo
def is_fully_allocated(self) -> bool:
"""Return True if this BuildLine is fully allocated."""
if self.bom_item.consumable:
if self.bom_item.is_consumable:
return True
required = max(0, self.quantity - self.consumed)

View File

@ -975,7 +975,7 @@ class BuildAllocationSerializer(serializers.Serializer):
output = item.get('output', None)
# Ignore allocation for consumable BOM items
if build_line.bom_item.consumable:
if build_line.bom_item.is_consumable:
continue
params = {
@ -1368,7 +1368,7 @@ class BuildLineSerializer(
source='bom_item.reference', label=_('Reference'), read_only=True
)
consumable = serializers.BooleanField(
source='bom_item.consumable', label=_('Consumable'), read_only=True
source='bom_item.is_consumable', label=_('Consumable'), read_only=True
)
optional = serializers.BooleanField(
source='bom_item.optional', label=_('Optional'), read_only=True

View File

@ -1626,7 +1626,7 @@ class Part(
queryset = self.get_bom_items(include_virtual=False)
# Ignore 'consumable' BOM items for this calculation
queryset = queryset.filter(consumable=False)
queryset = queryset.filter(BomItem.consumable_filter(consumable=False))
# Annotate the queryset with the 'can_build' quantity
queryset = part.filters.annotate_bom_item_can_build(queryset)
@ -4295,6 +4295,35 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
return self.get_item_hash() == self.checksum
@property
def is_consumable(self) -> bool:
"""Return True if this BOM line should be treated as consumable.
This is the case if either:
- The BOM line itself is marked as consumable
- The underlying part is marked as consumable
"""
return self.consumable or self.sub_part.consumable
@staticmethod
def consumable_filter(consumable: bool = True, prefix: str = '') -> Q:
"""Return a Q filter which selects BomItem objects based on "effective" consumable status.
A BomItem is considered "effectively consumable" if either the BOM line itself,
or the underlying part, is marked as consumable.
Arguments:
consumable: If True, return a filter which matches consumable BOM items.
If False, return a filter which matches non-consumable BOM items.
prefix: Optional field lookup prefix, for use against querysets of a
related model (e.g. 'bom_item__' when filtering a BuildLine queryset).
"""
f = Q(**{f'{prefix}consumable': True}) | Q(**{
f'{prefix}sub_part__consumable': True
})
return f if consumable else ~f
def clean(self):
"""Check validity of the BomItem model.

View File

@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
import structlog
import part.models
from build.events import BuildEvents
from build.models import Build
from build.status_codes import BuildStatus
@ -56,7 +57,8 @@ class AutoCreateBuildsPlugin(EventMixin, InvenTreePlugin):
"""Process a build order when it is issued."""
# Iterate through all sub-assemblies for the build order
for bom_item in build.part.get_bom_items().filter(
consumable=False, sub_part__assembly=True
part.models.BomItem.consumable_filter(consumable=False),
sub_part__assembly=True,
):
subassembly = bom_item.sub_part