2
0
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:
Oliver
2022-09-24 23:45:56 +10:00
committed by GitHub
parent a7e4d27d6d
commit 1b421fb59a
14 changed files with 256 additions and 99 deletions

View File

@ -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'

View 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'),
),
]

View File

@ -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):

View File

@ -760,6 +760,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
'inherited',
'note',
'optional',
'consumable',
'overage',
'pk',
'part',

View File

@ -118,6 +118,7 @@ class BomExportTest(InvenTreeTestCase):
'sub_assembly',
'quantity',
'optional',
'consumable',
'overage',
'reference',
'note',

View File

@ -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)