From 92ac93aac54c23dc9a0e3980da13481fa1f0f101 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Aug 2020 14:17:59 +1000 Subject: [PATCH] More intelligent checking for circular BOM - Check all the way down a BOM "tree" - Validate BOM tree before allowing BOM submission --- InvenTree/part/models.py | 54 ++++++++++++++++++++++++++++------------ InvenTree/part/views.py | 10 +++++++- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index cbc22c5605..dd126d5730 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -271,6 +271,42 @@ class Part(MPTTModel): def __str__(self): return "{n} - {d}".format(n=self.full_name, d=self.description) + def checkAddToBOM(self, parent): + """ + Check if this Part can be added to the BOM of another part. + + This will fail if: + + a) The parent part is the same as this one + 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 + + 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) + ))}) + + # Ensure that the parent part does not appear under any child BOM item! + for item in self.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 + item.sub_part.checkAddToBOM(parent) + def checkIfSerialNumberExists(self, sn): """ Check if a serial number exists for this Part. @@ -1474,22 +1510,8 @@ class BomItem(models.Model): except Part.DoesNotExist: pass - # A part cannot refer to itself in its BOM - try: - if self.sub_part is not None and self.part is not None: - if self.part == self.sub_part: - raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')}) - - # TODO - Make sure that there is no recusion - - # Test for simple recursion - for item in self.sub_part.bom_items.all(): - if self.part == item.sub_part: - raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)".format(p1=str(self.part), p2=str(self.sub_part)))}) - - except Part.DoesNotExist: - # A blank Part will be caught elsewhere - pass + # Check for circular BOM references + self.sub_part.checkAddToBOM(self.part) class Meta: verbose_name = _("BOM Item") diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 9f35dbbb10..a01b6b4ff2 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1325,8 +1325,16 @@ class BomUpload(FormView): for row in self.bom_rows: # Has a part been selected for the given row? - if row.get('part', None) is None: + part = row.get('part', None) + + if part is None: row['errors']['part'] = _('Select a part') + else: + # Will the selected part result in a recursive BOM? + try: + part.checkAddToBOM(self.part) + except ValidationError: + row['errors']['part'] = _('Selected part creates a circular BOM') # Has a quantity been specified? if row.get('quantity', None) is None: