diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index 198e58e337..2f72537136 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -16,8 +16,15 @@ class PartConfig(AppConfig): """ self.generate_part_thumbnails() + self.update_trackable_status() def generate_part_thumbnails(self): + """ + Generate thumbnail images for any Part that does not have one. + This function exists mainly for legacy support, + as any *new* image uploaded will have a thumbnail generated automatically. + """ + from .models import Part print("InvenTree: Checking Part image thumbnails") @@ -37,4 +44,27 @@ class PartConfig(AppConfig): part.image = None part.save() except (OperationalError, ProgrammingError): + # Exception if the database has not been migrated yet + pass + + def update_trackable_status(self): + """ + Check for any instances where a trackable part is used in the BOM + for a non-trackable part. + + In such a case, force the top-level part to be trackable too. + """ + + from .models import BomItem + + try: + items = BomItem.objects.filter(part__trackable=False, sub_part__trackable=True) + + for item in items: + print(f"Marking part '{item.part.name}' as trackable") + item.part.trackable = True + item.part.clean() + item.part.save() + except (OperationalError, ProgrammingError): + # Exception if the database has not been migrated yet pass diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index aeced2f6f0..2e9b75d14f 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -529,10 +529,24 @@ class Part(MPTTModel): pass def clean(self): - """ Perform cleaning operations for the Part model """ + """ + Perform cleaning operations for the Part model + + Update trackable status: + If this part is trackable, and it is used in the BOM + for a parent part which is *not* trackable, + then we will force the parent part to be trackable. + """ super().clean() + if self.trackable: + for parent_part in self.used_in.all(): + if not parent_part.trackable: + parent_part.trackable = True + parent_part.clean() + parent_part.save() + name = models.CharField(max_length=100, blank=False, help_text=_('Part name'), validators=[validators.validate_part_name] @@ -1612,12 +1626,15 @@ class BomItem(models.Model): return self.get_item_hash() == self.checksum def clean(self): - """ Check validity of the BomItem model. + """ + Check validity of the BomItem model. Performs model checks beyond simple field validation. - A part cannot refer to itself in its BOM - A part cannot refer to a part which refers to it + + - If the "sub_part" is trackable, then the "part" must be trackable too! """ # If the sub_part is 'trackable' then the 'quantity' field must be an integer @@ -1627,6 +1644,13 @@ class BomItem(models.Model): raise ValidationError({ "quantity": _("Quantity must be integer value for trackable parts") }) + + # Force the upstream part to be trackable if the sub_part is trackable + if not self.part.trackable: + self.part.trackable = True + self.part.clean() + self.part.save() + except Part.DoesNotExist: pass