diff --git a/InvenTree/part/migrations/0078_auto_20220606_0024.py b/InvenTree/part/migrations/0078_auto_20220606_0024.py new file mode 100644 index 000000000000..385eb591ce5b --- /dev/null +++ b/InvenTree/part/migrations/0078_auto_20220606_0024.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.13 on 2022-06-06 00:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0077_alter_bomitem_unique_together'), + ] + + operations = [ + migrations.AlterField( + model_name='partrelated', + name='part_1', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='related_parts_1', to='part.part', verbose_name='Part 1'), + ), + migrations.AlterField( + model_name='partrelated', + name='part_2', + field=models.ForeignKey(help_text='Select Related Part', on_delete=django.db.models.deletion.CASCADE, related_name='related_parts_2', to='part.part', verbose_name='Part 2'), + ), + migrations.AlterUniqueTogether( + name='partrelated', + unique_together={('part_1', 'part_2')}, + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 912541fbc028..ebe9f882d6f8 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2037,27 +2037,20 @@ def get_conversion_options(self): return filtered_parts def get_related_parts(self): - """Return list of tuples for all related parts. - - Includes: - - first value is PartRelated object - - second value is matching Part object - """ - related_parts = [] + """Return a set of all related parts for this part""" + related_parts = set() 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) - related_parts.append() - for related_part in related_parts_1: # Add to related parts list - related_parts.append(related_part.part_2) + related_parts.add(related_part.part_2) for related_part in related_parts_2: # Add to related parts list - related_parts.append(related_part.part_1) + related_parts.add(related_part.part_1) return related_parts @@ -2829,44 +2822,35 @@ def get_api_url(): class PartRelated(models.Model): """Store and handle related parts (eg. mating connector, crimps, etc.).""" + class Meta: + """Metaclass defines extra model properties""" + unique_together = ('part_1', 'part_2') + part_1 = models.ForeignKey(Part, related_name='related_parts_1', - verbose_name=_('Part 1'), on_delete=models.DO_NOTHING) + verbose_name=_('Part 1'), on_delete=models.CASCADE) part_2 = models.ForeignKey(Part, related_name='related_parts_2', - on_delete=models.DO_NOTHING, + on_delete=models.CASCADE, verbose_name=_('Part 2'), help_text=_('Select Related Part')) def __str__(self): """Return a string representation of this Part-Part relationship""" 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 save(self, *args, **kwargs): + """Enforce a 'clean' operation when saving a PartRelated instance""" + self.clean() + self.validate_unique() + super().save(*args, **kwargs) 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') + super().clean() + + if self.part_1 == self.part_2: + raise ValidationError(_("Part relationship cannot be created between a part and itself")) - raise ValidationError(error_message) + # Check for inverse relationship + if PartRelated.objects.filter(part_1=self.part_2, part_2=self.part_1).exists(): + raise ValidationError(_("Duplicate relationship already exists")) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index bcf98bef9c1f..e940d46926e1 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -559,13 +559,13 @@
{% if roles.part.delete %} $("#part-delete").click(function() { - launchModalForm( - "{% url 'part-delete' part.id %}", - { - redirect: {% if part.category %}"{% url 'category-detail' part.category.id %}"{% else %}"{% url 'part-index' %}"{% endif %}, - no_post: {% if part.active %}true{% else %}false{% endif %}, - } - ); + deletePart({{ part.pk }}, { + {% if part.category %} + redirect: '{% url "category-detail" part.category.pk %}', + {% else %} + redirect: '{% url "part-index" %}', + {% endif %} + }); }); {% endif %} diff --git a/InvenTree/part/templates/part/partial_delete.html b/InvenTree/part/templates/part/partial_delete.html deleted file mode 100644 index cd83f8df4f9b..000000000000 --- a/InvenTree/part/templates/part/partial_delete.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "modal_form.html" %} -{% load i18n %} - -{% block pre_form_content %} - -{% if part.active %} - -
- {% blocktrans with full_name=part.full_name %}Part '{{full_name}}' cannot be deleted as it is still marked as active. -
Disable the "Active" part attribute and re-try. - {% endblocktrans %} -
- -{% else %} - -
- {% blocktrans with full_name=part.full_name %}Are you sure you want to delete part '{{full_name}}'?{% endblocktrans %} -
- -{% if part.used_in_count %} -
-

{% blocktrans with count=part.used_in_count %}This part is used in BOMs for {{count}} other parts. If you delete this part, the BOMs for the following parts will be updated{% endblocktrans %}: -