mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	Furher logic improvements to BOM copy
- Remove "self" part from list - Stop inherited BOM items from being copied incorrectly - Allow user to select whether "inherited" BOM items are copied
This commit is contained in:
		@@ -481,7 +481,7 @@ class Part(MPTTModel):
 | 
				
			|||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"{self.full_name} - {self.description}"
 | 
					        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.
 | 
					        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
 | 
					        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
 | 
					        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:
 | 
					        result = True
 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.pk == parent.pk:
 | 
					        try:
 | 
				
			||||||
            raise ValidationError({'sub_part': _("Part '{p1}' is  used in BOM for '{p2}' (recursive)").format(
 | 
					            if self.pk == parent.pk:
 | 
				
			||||||
                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:
 | 
					 | 
				
			||||||
                raise ValidationError({'sub_part': _("Part '{p1}' is  used in BOM for '{p2}' (recursive)").format(
 | 
					                raise ValidationError({'sub_part': _("Part '{p1}' is  used in BOM for '{p2}' (recursive)").format(
 | 
				
			||||||
                    p1=str(parent),
 | 
					                    p1=str(self),
 | 
				
			||||||
                    p2=str(self)
 | 
					                    p2=str(parent)
 | 
				
			||||||
                )})
 | 
					                )})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # And recursively check too
 | 
					            bom_items = self.get_bom_items()
 | 
				
			||||||
            item.sub_part.checkAddToBOM(parent)
 | 
					
 | 
				
			||||||
 | 
					            # 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):
 | 
					    def checkIfSerialNumberExists(self, sn, exclude_self=False):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -1816,23 +1827,45 @@ class Part(MPTTModel):
 | 
				
			|||||||
            clear - Remove existing BOM items first (default=True)
 | 
					            clear - Remove existing BOM items first (default=True)
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Ignore if the other part is actually this part?
 | 
				
			||||||
 | 
					        if other == self:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if clear:
 | 
					        if clear:
 | 
				
			||||||
            # Remove existing BOM items
 | 
					            # Remove existing BOM items
 | 
				
			||||||
            # Note: Inherited BOM items are *not* deleted!
 | 
					            # Note: Inherited BOM items are *not* deleted!
 | 
				
			||||||
            self.bom_items.all().delete()
 | 
					            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
 | 
					        # Copy existing BOM items from another part
 | 
				
			||||||
        # Note: Inherited BOM Items will *not* be duplicated!!
 | 
					        # 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,
 | 
					            # If this part already has a BomItem pointing to the same sub-part,
 | 
				
			||||||
            # delete that BomItem from this part first!
 | 
					            # delete that BomItem from this part first!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try:
 | 
					            # Ignore invalid BomItem objects
 | 
				
			||||||
                existing = BomItem.objects.get(part=self, sub_part=bom_item.sub_part)
 | 
					            if not bom_item.part or not bom_item.sub_part:
 | 
				
			||||||
                existing.delete()
 | 
					                continue
 | 
				
			||||||
            except (BomItem.DoesNotExist):
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # 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.part = self
 | 
				
			||||||
            bom_item.pk = None
 | 
					            bom_item.pk = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2677,7 +2710,7 @@ class BomItem(models.Model):
 | 
				
			|||||||
        try:
 | 
					        try:
 | 
				
			||||||
            # Check for circular BOM references
 | 
					            # Check for circular BOM references
 | 
				
			||||||
            if self.sub_part:
 | 
					            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 the sub_part is 'trackable' then the 'quantity' field must be an integer
 | 
				
			||||||
                if self.sub_part.trackable:
 | 
					                if self.sub_part.trackable:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -664,14 +664,24 @@ class PartCopyBOMSerializer(serializers.Serializer):
 | 
				
			|||||||
        Check that a 'valid' part was selected
 | 
					        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
 | 
					        return part
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    remove_existing = serializers.BooleanField(
 | 
					    remove_existing = serializers.BooleanField(
 | 
				
			||||||
        label=_('Remove Existing Data'),
 | 
					        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):
 | 
					    def save(self):
 | 
				
			||||||
@@ -683,7 +693,9 @@ class PartCopyBOMSerializer(serializers.Serializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        part = data['part']
 | 
					        base_part.copy_bom_from(
 | 
				
			||||||
        clear = data.get('remove_existing', True)
 | 
					            data['part'],
 | 
				
			||||||
 | 
					            clear=data.get('remove_existing', True),
 | 
				
			||||||
        base_part.copy_bom_from(part, clear=clear)
 | 
					            skip_invalid=data.get('skip_invalid', False),
 | 
				
			||||||
 | 
					            include_inherited=data.get('include_inherited', False),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -582,7 +582,9 @@
 | 
				
			|||||||
        $('#bom-duplicate').click(function() {
 | 
					        $('#bom-duplicate').click(function() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            duplicateBom({{ part.pk }}, {
 | 
					            duplicateBom({{ part.pk }}, {
 | 
				
			||||||
 | 
					                success: function(response) {
 | 
				
			||||||
 | 
					                    $('#bom-table').bootstrapTable('refresh');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -661,7 +661,7 @@ function loadBomTable(table, options={}) {
 | 
				
			|||||||
            if (!row.inherited) {
 | 
					            if (!row.inherited) {
 | 
				
			||||||
                return yesNoLabel(false);
 | 
					                return yesNoLabel(false);
 | 
				
			||||||
            } else if (row.part == options.parent_id) {
 | 
					            } else if (row.part == options.parent_id) {
 | 
				
			||||||
                return '{% trans "Inherited" %}';
 | 
					                return yesNoLabel(true);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                // If this BOM item is inherited from a parent part
 | 
					                // If this BOM item is inherited from a parent part
 | 
				
			||||||
                return renderLink(
 | 
					                return renderLink(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -438,15 +438,20 @@ function duplicateBom(part_id, options={}) {
 | 
				
			|||||||
                icon: 'fa-shapes',
 | 
					                icon: 'fa-shapes',
 | 
				
			||||||
                filters: {
 | 
					                filters: {
 | 
				
			||||||
                    assembly: true,
 | 
					                    assembly: true,
 | 
				
			||||||
                    ancestor: part_id,
 | 
					                    exclude_tree: part_id,
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            remove_existing: {
 | 
					            include_inherited: {},
 | 
				
			||||||
                value: true,
 | 
					            remove_existing: {},
 | 
				
			||||||
            },
 | 
					            skip_invalid: {},
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        confirm: true,
 | 
					        confirm: true,
 | 
				
			||||||
        title: '{% trans "Copy Bill of Materials" %}',
 | 
					        title: '{% trans "Copy Bill of Materials" %}',
 | 
				
			||||||
 | 
					        onSuccess: function(response) {
 | 
				
			||||||
 | 
					            if (options.success) {
 | 
				
			||||||
 | 
					                options.success(response);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user