diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 834ac93eed..dccc2f9ac1 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -1281,7 +1281,6 @@ class BomItemValidate(generics.UpdateAPIView): return Response(serializer.data) - class BomItemSubstituteList(generics.ListCreateAPIView): """ API endpoint for accessing a list of BomItemSubstitute objects diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 85dd9042fc..75a8249f7b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2629,6 +2629,12 @@ class BomItemSubstitute(models.Model): # Prevent duplication of substitute parts unique_together = ('part', 'bom_item') + def save(self, *args, **kwargs): + + self.full_clean() + + super().save(*args, **kwargs) + def validate_unique(self, exclude=None): """ Ensure that this BomItemSubstitute is "unique": diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py index be9740d128..cd3c77845c 100644 --- a/InvenTree/part/test_bom_item.py +++ b/InvenTree/part/test_bom_item.py @@ -1,8 +1,13 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from django.db import transaction + from django.test import TestCase import django.core.exceptions as django_exceptions from decimal import Decimal -from .models import Part, BomItem +from .models import Part, BomItem, BomItemSubstitute class BomItemTest(TestCase): @@ -130,3 +135,67 @@ class BomItemTest(TestCase): self.bob.get_bom_price_range(1, internal=True), (Decimal(27.5), Decimal(87.5)) ) + + def test_substitutes(self): + """ + Tests for BOM item substitutes + """ + + # We will make some subtitute parts for the "orphan" part + bom_item = BomItem.objects.get( + part=self.bob, + sub_part=self.orphan + ) + + # No substitute parts available + self.assertEqual(bom_item.substitutes.count(), 0) + + subs = [] + + for ii in range(5): + + # Create a new part + sub_part = Part.objects.create( + name=f"Orphan {ii}", + description="A substitute part for the orphan part", + component=True, + is_template=False, + assembly=False, + ) + + subs.append(sub_part) + + # Link it as a substitute part + BomItemSubstitute.objects.create( + bom_item=bom_item, + part=sub_part + ) + + # Try to link it again (this should fail as it is a duplicate substitute) + with self.assertRaises(django_exceptions.ValidationError): + with transaction.atomic(): + BomItemSubstitute.objects.create( + bom_item=bom_item, + part=sub_part + ) + + # There should be now 5 substitute parts available + self.assertEqual(bom_item.substitutes.count(), 5) + + # Try to create a substitute which points to the same sub-part (should fail) + with self.assertRaises(django_exceptions.ValidationError): + BomItemSubstitute.objects.create( + bom_item=bom_item, + part=self.orphan, + ) + + # Remove one substitute part + bom_item.substitutes.last().delete() + + self.assertEqual(bom_item.substitutes.count(), 4) + + for sub in subs: + sub.delete() + + # The substitution links should have been automatically removed + self.assertEqual(bom_item.substitutes.count(), 0) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index c0dbdd4453..9d3380c653 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -161,7 +161,9 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { var pk = substitute.pk; - var thumb = thumbnailImage(substitute.part_detail.thumbnail || substitute.part_detail.image); + var part = substitute.part_detail; + + var thumb = thumbnailImage(part.thumbnail || part.image); var buttons = ''; @@ -170,8 +172,12 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { // Render a single row var html = `