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:
Oliver 2021-12-21 22:07:08 +11:00
parent 0c8a047bc2
commit 70f9a0fe13
5 changed files with 95 additions and 43 deletions

View File

@ -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,13 +491,11 @@ 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
try:
if self.pk == parent.pk: if self.pk == parent.pk:
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(self), p1=str(self),
@ -517,7 +515,20 @@ class Part(MPTTModel):
)}) )})
# And recursively check too # And recursively check too
item.sub_part.checkAddToBOM(parent) 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:

View File

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

View File

@ -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');
}
}); });
}); });

View File

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

View File

@ -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);
}
},
}); });
} }