mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-10 23:14:13 +00:00
Merge branch 'master' into categories_parameters
This commit is contained in:
@ -564,10 +564,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]
|
||||
@ -909,6 +923,19 @@ class Part(MPTTModel):
|
||||
def has_bom(self):
|
||||
return self.bom_count > 0
|
||||
|
||||
@property
|
||||
def has_trackable_parts(self):
|
||||
"""
|
||||
Return True if any parts linked in the Bill of Materials are trackable.
|
||||
This is important when building the part.
|
||||
"""
|
||||
|
||||
for bom_item in self.bom_items.all():
|
||||
if bom_item.sub_part.trackable:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def bom_count(self):
|
||||
""" Return the number of items contained in the BOM for this part """
|
||||
@ -966,15 +993,31 @@ class Part(MPTTModel):
|
||||
|
||||
self.bom_items.all().delete()
|
||||
|
||||
def required_parts(self):
|
||||
""" Return a list of parts required to make this part (list of BOM items) """
|
||||
parts = []
|
||||
for bom in self.bom_items.all().select_related('sub_part'):
|
||||
parts.append(bom.sub_part)
|
||||
def getRequiredParts(self, recursive=False, parts=set()):
|
||||
"""
|
||||
Return a list of parts required to make this part (i.e. BOM items).
|
||||
|
||||
Args:
|
||||
recursive: If True iterate down through sub-assemblies
|
||||
parts: Set of parts already found (to prevent recursion issues)
|
||||
"""
|
||||
|
||||
for bom_item in self.bom_items.all().select_related('sub_part'):
|
||||
|
||||
sub_part = bom_item.sub_part
|
||||
|
||||
if sub_part not in parts:
|
||||
|
||||
parts.add(sub_part)
|
||||
|
||||
if recursive:
|
||||
sub_part.getRequiredParts(recursive=True, parts=parts)
|
||||
|
||||
return parts
|
||||
|
||||
def get_allowed_bom_items(self):
|
||||
""" Return a list of parts which can be added to a BOM for this part.
|
||||
"""
|
||||
Return a list of parts which can be added to a BOM for this part.
|
||||
|
||||
- Exclude parts which are not 'component' parts
|
||||
- Exclude parts which this part is in the BOM for
|
||||
@ -1176,7 +1219,7 @@ class Part(MPTTModel):
|
||||
parameter.save()
|
||||
|
||||
@transaction.atomic
|
||||
def deepCopy(self, other, **kwargs):
|
||||
def deep_copy(self, other, **kwargs):
|
||||
""" Duplicates non-field data from another part.
|
||||
Does not alter the normal fields of this part,
|
||||
but can be used to copy other data linked by ForeignKey refernce.
|
||||
@ -1331,6 +1374,32 @@ class Part(MPTTModel):
|
||||
|
||||
return self.get_descendants(include_self=False)
|
||||
|
||||
def get_related_parts(self):
|
||||
""" Return list of tuples for all related parts:
|
||||
- first value is PartRelated object
|
||||
- second value is matching Part object
|
||||
"""
|
||||
|
||||
related_parts = []
|
||||
|
||||
related_parts_1 = self.related_parts_1.filter(part_1__id=self.pk)
|
||||
|
||||
related_parts_2 = self.related_parts_2.filter(part_2__id=self.pk)
|
||||
|
||||
for related_part in related_parts_1:
|
||||
# Add to related parts list
|
||||
related_parts.append((related_part, related_part.part_2))
|
||||
|
||||
for related_part in related_parts_2:
|
||||
# Add to related parts list
|
||||
related_parts.append((related_part, related_part.part_1))
|
||||
|
||||
return related_parts
|
||||
|
||||
@property
|
||||
def related_count(self):
|
||||
return len(self.get_related_parts())
|
||||
|
||||
|
||||
def attach_file(instance, filename):
|
||||
""" Function for storing a file for a PartAttachment
|
||||
@ -1715,12 +1784,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
|
||||
@ -1730,6 +1802,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
|
||||
|
||||
@ -1842,3 +1921,71 @@ class BomItem(models.Model):
|
||||
pmax = decimal2string(pmax)
|
||||
|
||||
return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax)
|
||||
|
||||
|
||||
class PartRelated(models.Model):
|
||||
""" Store and handle related parts (eg. mating connector, crimps, etc.) """
|
||||
|
||||
part_1 = models.ForeignKey(Part, related_name='related_parts_1',
|
||||
on_delete=models.DO_NOTHING)
|
||||
|
||||
part_2 = models.ForeignKey(Part, related_name='related_parts_2',
|
||||
on_delete=models.DO_NOTHING,
|
||||
help_text=_('Select Related Part'))
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.part_1} <--> {self.part_2}'
|
||||
|
||||
def validate(self, part_1, part_2):
|
||||
''' Validate that the two parts relationship is unique '''
|
||||
|
||||
validate = True
|
||||
|
||||
parts = Part.objects.all()
|
||||
related_parts = PartRelated.objects.all()
|
||||
|
||||
# Check if part exist and there are not the same part
|
||||
if (part_1 in parts and part_2 in parts) and (part_1.pk != part_2.pk):
|
||||
# Check if relation exists already
|
||||
for relation in related_parts:
|
||||
if (part_1 == relation.part_1 and part_2 == relation.part_2) \
|
||||
or (part_1 == relation.part_2 and part_2 == relation.part_1):
|
||||
validate = False
|
||||
break
|
||||
else:
|
||||
validate = False
|
||||
|
||||
return validate
|
||||
|
||||
def clean(self):
|
||||
''' Overwrite clean method to check that relation is unique '''
|
||||
|
||||
validate = self.validate(self.part_1, self.part_2)
|
||||
|
||||
if not validate:
|
||||
error_message = _('Error creating relationship: check that '
|
||||
'the part is not related to itself '
|
||||
'and that the relationship is unique')
|
||||
|
||||
raise ValidationError(error_message)
|
||||
|
||||
def create_relationship(self, part_1, part_2):
|
||||
''' Create relationship between two parts '''
|
||||
|
||||
validate = self.validate(part_1, part_2)
|
||||
|
||||
if validate:
|
||||
# Add relationship
|
||||
self.part_1 = part_1
|
||||
self.part_2 = part_2
|
||||
self.save()
|
||||
|
||||
return validate
|
||||
|
||||
@classmethod
|
||||
def create(cls, part_1, part_2):
|
||||
''' Create PartRelated object and relationship between two parts '''
|
||||
|
||||
related_part = cls()
|
||||
related_part.create_relationship(part_1, part_2)
|
||||
return related_part
|
||||
|
Reference in New Issue
Block a user