diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index f0c9e3f233..7476197547 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -9,7 +9,7 @@ from import_export.fields import Field import import_export.widgets as widgets from .models import PartCategory, Part -from .models import PartAttachment, PartStar +from .models import PartAttachment, PartStar, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartTestTemplate @@ -121,6 +121,11 @@ class PartCategoryAdmin(ImportExportModelAdmin): search_fields = ('name', 'description') +class PartRelatedAdmin(admin.ModelAdmin): + ''' Class to manage PartRelated objects ''' + pass + + class PartAttachmentAdmin(admin.ModelAdmin): list_display = ('part', 'attachment', 'comment') @@ -279,6 +284,7 @@ class PartSellPriceBreakAdmin(admin.ModelAdmin): admin.site.register(Part, PartAdmin) admin.site.register(PartCategory, PartCategoryAdmin) +admin.site.register(PartRelated, PartRelatedAdmin) admin.site.register(PartAttachment, PartAttachmentAdmin) admin.site.register(PartStar, PartStarAdmin) admin.site.register(BomItem, BomItemAdmin) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 52c39bf3ba..c4523113e5 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -13,7 +13,7 @@ from mptt.fields import TreeNodeChoiceField from django import forms from django.utils.translation import ugettext as _ -from .models import Part, PartCategory, PartAttachment +from .models import Part, PartCategory, PartAttachment, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartTestTemplate @@ -141,6 +141,25 @@ class BomUploadSelectFile(HelperForm): ] +class CreatePartRelatedForm(HelperForm): + """ Form for creating a PartRelated object """ + + class Meta: + model = PartRelated + fields = [ + 'part_1', + 'part_2', + ] + labels = { + 'part_2': _('Related Part'), + } + + def save(self): + """ Disable model saving """ + + return super(CreatePartRelatedForm, self).save(commit=False) + + class EditPartAttachmentForm(HelperForm): """ Form for editing a PartAttachment object """ diff --git a/InvenTree/part/migrations/0052_partrelated.py b/InvenTree/part/migrations/0052_partrelated.py new file mode 100644 index 0000000000..a8672ba7dc --- /dev/null +++ b/InvenTree/part/migrations/0052_partrelated.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.7 on 2020-10-16 20:42 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0051_bomitem_optional'), + ] + + operations = [ + migrations.CreateModel( + name='PartRelated', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('part_1', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_1', to='part.Part')), + ('part_2', models.ForeignKey(help_text='Select Related Part', on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_2', to='part.Part')), + ], + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 715d1423eb..707bc34ca2 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1313,6 +1313,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 @@ -1782,3 +1808,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 diff --git a/InvenTree/part/templates/part/related.html b/InvenTree/part/templates/part/related.html new file mode 100644 index 0000000000..8c5cc074c8 --- /dev/null +++ b/InvenTree/part/templates/part/related.html @@ -0,0 +1,77 @@ +{% extends "part/part_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include 'part/tabs.html' with tab='related-parts' %} + +
{% trans "Part" %} | +
---|
+
+
+ {% if roles.part.change %}
+
+ {% endif %}
+
+ |
+