diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 5c6c07d9d8..b1aee26094 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -481,7 +481,7 @@ class Part(MPTTModel): def __str__(self): return f"{self.full_name} - {self.description}" - def checkAddToBOM(self, parent): + def check_add_to_bom(self, parent, raise_error=False, recursive=True): """ Check if this Part can be added to the BOM of another part. @@ -491,33 +491,44 @@ class Part(MPTTModel): b) The parent part is used in the BOM for *this* part c) The parent part is used in the BOM for any child parts under this one - Failing this check raises a ValidationError! - """ - if parent is None: - return + result = True - if self.pk == parent.pk: - raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)").format( - p1=str(self), - p2=str(parent) - )}) - - bom_items = self.get_bom_items() - - # Ensure that the parent part does not appear under any child BOM item! - for item in bom_items.all(): - - # Check for simple match - if item.sub_part == parent: + try: + if self.pk == parent.pk: raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)").format( - p1=str(parent), - p2=str(self) + p1=str(self), + p2=str(parent) )}) - # And recursively check too - item.sub_part.checkAddToBOM(parent) + bom_items = self.get_bom_items() + + # Ensure that the parent part does not appear under any child BOM item! + for item in bom_items.all(): + + # Check for simple match + if item.sub_part == parent: + raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)").format( + p1=str(parent), + p2=str(self) + )}) + + # And recursively check too + if recursive: + result = result and item.sub_part.check_add_to_bom( + parent, + recursive=True, + raise_error=raise_error + ) + + except ValidationError as e: + if raise_error: + raise e + else: + return False + + return result def checkIfSerialNumberExists(self, sn, exclude_self=False): """ @@ -1816,23 +1827,45 @@ class Part(MPTTModel): clear - Remove existing BOM items first (default=True) """ + # Ignore if the other part is actually this part? + if other == self: + return + if clear: # Remove existing BOM items # Note: Inherited BOM items are *not* deleted! self.bom_items.all().delete() + # List of "ancestor" parts above this one + my_ancestors = self.get_ancestors(include_self=False) + + raise_error = not kwargs.get('skip_invalid', True) + + include_inherited = kwargs.get('include_inherited', False) + # Copy existing BOM items from another part # Note: Inherited BOM Items will *not* be duplicated!! - for bom_item in other.get_bom_items(include_inherited=False).all(): + for bom_item in other.get_bom_items(include_inherited=include_inherited).all(): # If this part already has a BomItem pointing to the same sub-part, # delete that BomItem from this part first! - try: - existing = BomItem.objects.get(part=self, sub_part=bom_item.sub_part) - existing.delete() - except (BomItem.DoesNotExist): - pass + # Ignore invalid BomItem objects + if not bom_item.part or not bom_item.sub_part: + continue + # Ignore ancestor parts which are inherited + if bom_item.part in my_ancestors and bom_item.inherited: + continue + + # Skip if already exists + if BomItem.objects.filter(part=self, sub_part=bom_item.sub_part).exists(): + continue + + # Skip (or throw error) if BomItem is not valid + if not bom_item.sub_part.check_add_to_bom(self, raise_error=raise_error): + continue + + # Construct a new BOM item bom_item.part = self bom_item.pk = None @@ -2677,7 +2710,7 @@ class BomItem(models.Model): try: # Check for circular BOM references if self.sub_part: - self.sub_part.checkAddToBOM(self.part) + self.sub_part.check_add_to_bom(self.part, raise_error=True) # If the sub_part is 'trackable' then the 'quantity' field must be an integer if self.sub_part.trackable: diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 79e6596124..16ecc8da21 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -664,14 +664,24 @@ class PartCopyBOMSerializer(serializers.Serializer): Check that a 'valid' part was selected """ - # Check if the BOM can be copied from the provided part - base_part = self.context['part'] - return part remove_existing = serializers.BooleanField( label=_('Remove Existing Data'), - help_text=_('Remove existing BOM items before copying') + help_text=_('Remove existing BOM items before copying'), + default=True, + ) + + include_inherited = serializers.BooleanField( + label=_('Include Inherited'), + help_text=_('Include BOM items which are inherited from templated parts'), + default=False, + ) + + skip_invalid = serializers.BooleanField( + label=_('Skip Invalid Rows'), + help_text=_('Enable this option to skip invalid rows'), + default=False, ) def save(self): @@ -683,7 +693,9 @@ class PartCopyBOMSerializer(serializers.Serializer): data = self.validated_data - part = data['part'] - clear = data.get('remove_existing', True) - - base_part.copy_bom_from(part, clear=clear) + base_part.copy_bom_from( + data['part'], + clear=data.get('remove_existing', True), + skip_invalid=data.get('skip_invalid', False), + include_inherited=data.get('include_inherited', False), + ) diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 52c18b04c5..c768c61257 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -582,7 +582,9 @@ $('#bom-duplicate').click(function() { duplicateBom({{ part.pk }}, { - + success: function(response) { + $('#bom-table').bootstrapTable('refresh'); + } }); }); diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index ffd8195e07..aa44931a88 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -661,7 +661,7 @@ function loadBomTable(table, options={}) { if (!row.inherited) { return yesNoLabel(false); } else if (row.part == options.parent_id) { - return '{% trans "Inherited" %}'; + return yesNoLabel(true); } else { // If this BOM item is inherited from a parent part return renderLink( diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index f8a019d0e6..1cfd50d66c 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -438,15 +438,20 @@ function duplicateBom(part_id, options={}) { icon: 'fa-shapes', filters: { assembly: true, - ancestor: part_id, + exclude_tree: part_id, } }, - remove_existing: { - value: true, - }, + include_inherited: {}, + remove_existing: {}, + skip_invalid: {}, }, confirm: true, title: '{% trans "Copy Bill of Materials" %}', + onSuccess: function(response) { + if (options.success) { + options.success(response); + } + }, }); }