mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 13:05:42 +00:00
Adds 'consumable' field to BomItem model (#2890)
* Adds 'consumable' field to BomItem model * Add consumable field to API * Consumable items always count as "allocated" for a build * Add new BOM fields to BOM item checksum calculation * Display 'consumable' status in BOM table * Fix order of database migrations * Update unit tests * Fix for BOM table * Remove "infinite" field from StockItem model - Not used anywhere for functionality - Hidden from the user - Now replaced by the "consumable" concept in the BuildOrder model * Update build order allocation table display * Prevent auto-allocation of stock to consumable BOM items * Ignore consumable BOM items when allocating stock to a build order * Reimplmement "footer" row for BOM table * Fix "can_build" calculation - Ignore "consumable" BOM items * Unrelated typo fix * Tweak BOM table * More visual tweaks to BOM table * Add unit test for consumable field
This commit is contained in:
@ -1640,8 +1640,9 @@ class BomFilter(rest_filters.FilterSet):
|
||||
"""Custom filters for the BOM list."""
|
||||
|
||||
# Boolean filters for BOM item
|
||||
optional = rest_filters.BooleanFilter(label='BOM line is optional')
|
||||
inherited = rest_filters.BooleanFilter(label='BOM line is inherited')
|
||||
optional = rest_filters.BooleanFilter(label='BOM item is optional')
|
||||
consumable = rest_filters.BooleanFilter(label='BOM item is consumable')
|
||||
inherited = rest_filters.BooleanFilter(label='BOM item is inherited')
|
||||
allow_variants = rest_filters.BooleanFilter(label='Variants are allowed')
|
||||
|
||||
# Filters for linked 'part'
|
||||
|
18
InvenTree/part/migrations/0087_bomitem_consumable.py
Normal file
18
InvenTree/part/migrations/0087_bomitem_consumable.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.13 on 2022-04-28 00:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0086_auto_20220912_0007'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bomitem',
|
||||
name='consumable',
|
||||
field=models.BooleanField(default=False, help_text='This BOM item is consumable (it is not tracked in build orders)', verbose_name='Consumable'),
|
||||
),
|
||||
]
|
@ -1134,7 +1134,12 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
|
||||
total = None
|
||||
|
||||
# Prefetch related tables, to reduce query expense
|
||||
queryset = self.get_bom_items().prefetch_related(
|
||||
queryset = self.get_bom_items()
|
||||
|
||||
# Ignore 'consumable' BOM items for this calculation
|
||||
queryset = queryset.filter(consumable=False)
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
'sub_part__stock_items',
|
||||
'sub_part__stock_items__allocations',
|
||||
'sub_part__stock_items__sales_order_allocations',
|
||||
@ -2526,6 +2531,7 @@ class BomItem(DataImportMixin, models.Model):
|
||||
sub_part: Link to the child part (the part that will be consumed)
|
||||
quantity: Number of 'sub_parts' consumed to produce one 'part'
|
||||
optional: Boolean field describing if this BomItem is optional
|
||||
consumable: Boolean field describing if this BomItem is considered a 'consumable'
|
||||
reference: BOM reference field (e.g. part designators)
|
||||
overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%')
|
||||
note: Note field for this BOM item
|
||||
@ -2544,6 +2550,7 @@ class BomItem(DataImportMixin, models.Model):
|
||||
'allow_variants': {},
|
||||
'inherited': {},
|
||||
'optional': {},
|
||||
'consumable': {},
|
||||
'note': {},
|
||||
'part': {
|
||||
'label': _('Part'),
|
||||
@ -2649,7 +2656,17 @@ class BomItem(DataImportMixin, models.Model):
|
||||
# Quantity required
|
||||
quantity = models.DecimalField(default=1.0, max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], verbose_name=_('Quantity'), help_text=_('BOM quantity for this BOM item'))
|
||||
|
||||
optional = models.BooleanField(default=False, verbose_name=_('Optional'), help_text=_("This BOM item is optional"))
|
||||
optional = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Optional'),
|
||||
help_text=_("This BOM item is optional")
|
||||
)
|
||||
|
||||
consumable = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Consumable'),
|
||||
help_text=_("This BOM item is consumable (it is not tracked in build orders)")
|
||||
)
|
||||
|
||||
overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_overage],
|
||||
verbose_name=_('Overage'),
|
||||
@ -2698,6 +2715,14 @@ class BomItem(DataImportMixin, models.Model):
|
||||
result_hash.update(str(self.optional).encode())
|
||||
result_hash.update(str(self.inherited).encode())
|
||||
|
||||
# Optionally encoded for backwards compatibility
|
||||
if self.consumable:
|
||||
result_hash.update(str(self.consumable).encode())
|
||||
|
||||
# Optionally encoded for backwards compatibility
|
||||
if self.allow_variants:
|
||||
result_hash.update(str(self.allow_variants).encode())
|
||||
|
||||
return str(result_hash.digest())
|
||||
|
||||
def validate_hash(self, valid=True):
|
||||
|
@ -760,6 +760,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
'inherited',
|
||||
'note',
|
||||
'optional',
|
||||
'consumable',
|
||||
'overage',
|
||||
'pk',
|
||||
'part',
|
||||
|
@ -118,6 +118,7 @@ class BomExportTest(InvenTreeTestCase):
|
||||
'sub_assembly',
|
||||
'quantity',
|
||||
'optional',
|
||||
'consumable',
|
||||
'overage',
|
||||
'reference',
|
||||
'note',
|
||||
|
@ -7,6 +7,8 @@ import django.core.exceptions as django_exceptions
|
||||
from django.db import transaction
|
||||
from django.test import TestCase
|
||||
|
||||
import stock.models
|
||||
|
||||
from .models import BomItem, BomItemSubstitute, Part
|
||||
|
||||
|
||||
@ -197,3 +199,49 @@ class BomItemTest(TestCase):
|
||||
|
||||
# The substitution links should have been automatically removed
|
||||
self.assertEqual(bom_item.substitutes.count(), 0)
|
||||
|
||||
def test_consumable(self):
|
||||
"""Tests for the 'consumable' BomItem field"""
|
||||
|
||||
# Create an assembly part
|
||||
assembly = Part.objects.create(name="An assembly", description="Made with parts", assembly=True)
|
||||
|
||||
# No BOM information initially
|
||||
self.assertEqual(assembly.can_build, 0)
|
||||
|
||||
# Create some component items
|
||||
c1 = Part.objects.create(name="C1", description="C1")
|
||||
c2 = Part.objects.create(name="C2", description="C2")
|
||||
c3 = Part.objects.create(name="C3", description="C3")
|
||||
c4 = Part.objects.create(name="C4", description="C4")
|
||||
|
||||
for p in [c1, c2, c3, c4]:
|
||||
# Ensure we have stock
|
||||
stock.models.StockItem.objects.create(part=p, quantity=1000)
|
||||
|
||||
# Create some BOM items
|
||||
BomItem.objects.create(
|
||||
part=assembly,
|
||||
sub_part=c1,
|
||||
quantity=10
|
||||
)
|
||||
|
||||
self.assertEqual(assembly.can_build, 100)
|
||||
|
||||
BomItem.objects.create(
|
||||
part=assembly,
|
||||
sub_part=c2,
|
||||
quantity=50,
|
||||
consumable=True
|
||||
)
|
||||
|
||||
# A 'consumable' BomItem does not alter the can_build calculation
|
||||
self.assertEqual(assembly.can_build, 100)
|
||||
|
||||
BomItem.objects.create(
|
||||
part=assembly,
|
||||
sub_part=c3,
|
||||
quantity=50,
|
||||
)
|
||||
|
||||
self.assertEqual(assembly.can_build, 20)
|
||||
|
Reference in New Issue
Block a user