From 3d9223c2eedfbf914a65f3d2b074d84647c14b5f Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 15 Oct 2020 14:11:24 -0500 Subject: [PATCH 1/5] Introduced PartRelated model to store part relationships --- InvenTree/part/migrations/0052_partrelated.py | 22 ++++++++ InvenTree/part/models.py | 51 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 InvenTree/part/migrations/0052_partrelated.py diff --git a/InvenTree/part/migrations/0052_partrelated.py b/InvenTree/part/migrations/0052_partrelated.py new file mode 100644 index 0000000000..c84566b3ea --- /dev/null +++ b/InvenTree/part/migrations/0052_partrelated.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.7 on 2020-10-15 18:51 + +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(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 d427409c71..26ea555ed9 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1254,6 +1254,25 @@ class Part(MPTTModel): return self.get_descendants(include_self=False) + def fetch_related_parts(self): + """ Return all related parts """ + + related_parts = [] + + parts_1 = self.related_parts_1.filter(part_1__id=self.pk) + + parts_2 = self.related_parts_2.filter(part_2__id=self.pk) + + for part in parts_1: + # Append + related_parts.append(part.part_2) + + for part in parts_2: + # Append + related_parts.append(part.part_1) + + return related_parts + def attach_file(instance, filename): """ Function for storing a file for a PartAttachment @@ -1723,3 +1742,35 @@ 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) + + def create_relationship(self, part_1, part_2): + + parts = Part.objects.all() + + # Check if part exist + if (part_1 in parts and part_2 in parts) and (part_1 is not part_2): + # Add relationship + self.part_1 = part_1 + self.part_2 = part_2 + self.save() + + return True + else: + return False + + @classmethod + def create(cls, part_1, part_2): + related_part = cls() + related_part.create_relationship(part_1, part_2) + return related_part + + def __str__(self): + return f'{part_1} <-> {part_2}' From 8579abb9c2718ef6f25d3e6dfabf0537912f996d Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 15 Oct 2020 16:58:39 -0500 Subject: [PATCH 2/5] Added related parts urls, views, form and templates Adding related part relationships work but are still not shown in the part detail page --- InvenTree/part/forms.py | 21 ++++++- InvenTree/part/models.py | 33 +++++++---- InvenTree/part/templates/part/related.html | 46 +++++++++++++++ InvenTree/part/templates/part/tabs.html | 3 + InvenTree/part/urls.py | 9 +++ InvenTree/part/views.py | 69 +++++++++++++++++++++- InvenTree/templates/js/part.html | 35 +++++++++++ 7 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 InvenTree/part/templates/part/related.html diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index c64bbb8362..c463fda891 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 @@ -104,6 +104,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/models.py b/InvenTree/part/models.py index 26ea555ed9..aeef72da70 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1264,11 +1264,11 @@ class Part(MPTTModel): parts_2 = self.related_parts_2.filter(part_2__id=self.pk) for part in parts_1: - # Append + # Append related_parts.append(part.part_2) for part in parts_2: - # Append + # Append related_parts.append(part.part_1) return related_parts @@ -1749,28 +1749,41 @@ class PartRelated(models.Model): 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) + part_2 = models.ForeignKey(Part, related_name='related_parts_2', on_delete=models.DO_NOTHING, + help_text=_('Choose Related Part')) + + def __str__(self): + return f'{self.part_1} <-> {self.part_2}' def create_relationship(self, part_1, part_2): + ''' Create relationship between two parts ''' + + validate = True parts = Part.objects.all() + related_parts = PartRelated.objects.all() - # Check if part exist + # Check if part exist and there are not the same part if (part_1 in parts and part_2 in parts) and (part_1 is not part_2): + # 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 + else: + validate = False + + if validate: # Add relationship self.part_1 = part_1 self.part_2 = part_2 self.save() - return True - else: - return False + return validate @classmethod def create(cls, part_1, part_2): + ''' Create PartRelated object ''' related_part = cls() related_part.create_relationship(part_1, part_2) return related_part - - def __str__(self): - return f'{part_1} <-> {part_2}' diff --git a/InvenTree/part/templates/part/related.html b/InvenTree/part/templates/part/related.html new file mode 100644 index 0000000000..f9175ed590 --- /dev/null +++ b/InvenTree/part/templates/part/related.html @@ -0,0 +1,46 @@ +{% extends "part/part_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include 'part/tabs.html' with tab='related-parts' %} + +

{% trans "Related Parts" %}

+
+ +
+
+ + +
+
+ + + + + +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +loadPartRelatedTable($("#table-related-part"), { + url: "{% url 'api-part-list' %}", + params: { + part: {{ part.id }}, + }, +}); + +$("#add-related-part").click(function() { + launchModalForm("{% url 'part-related-create' %}", { + data: { + part: {{ part.id }}, + }, + reload: true, + }); +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 8322a225bc..c96de9b825 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -63,6 +63,9 @@ {% endif %} + + {% trans "Related Parts" %} {% if part.related_count > 0 %}{{ part.related_count }}{% endif %} + {% trans "Attachments" %} {% if part.attachment_count > 0 %}{{ part.attachment_count }}{% endif %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 32cd1b0615..e858e0583c 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -12,6 +12,11 @@ from django.conf.urls import url, include from . import views +part_related_urls = [ + url(r'^new/?', views.PartRelatedCreate.as_view(), name='part-related-create'), + url(r'^(?P\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'), +] + part_attachment_urls = [ url(r'^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'), url(r'^(?P\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'), @@ -60,6 +65,7 @@ part_detail_urls = [ url(r'^sale-prices/', views.PartDetail.as_view(template_name='part/sale_prices.html'), name='part-sale-prices'), url(r'^tests/', views.PartDetail.as_view(template_name='part/part_tests.html'), name='part-test-templates'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), + url(r'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'), @@ -112,6 +118,9 @@ part_urls = [ # Part category url(r'^category/(?P\d+)/', include(part_category_urls)), + # Part related + url(r'^related-parts/', include(part_related_urls)), + # Part attachments url(r'^attachment/', include(part_attachment_urls)), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 6498774285..f49d57bbe3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -21,7 +21,7 @@ import os from rapidfuzz import fuzz from decimal import Decimal, InvalidOperation -from .models import PartCategory, Part, PartAttachment +from .models import PartCategory, Part, PartAttachment, PartRelated from .models import PartParameterTemplate, PartParameter from .models import BomItem from .models import match_part_names @@ -70,6 +70,73 @@ class PartIndex(InvenTreeRoleMixin, ListView): return context +class PartRelatedCreate(AjaxCreateView): + """ View for creating a new PartRelated object + + - The view only makes sense if a Part object is passed to it + """ + model = PartRelated + form_class = part_forms.CreatePartRelatedForm + ajax_form_title = _("Add Related Part") + ajax_template_name = "modal_form.html" + role_required = 'part.change' + + # TODO: QuerySet should not show parts already related to object + + def get_initial(self): + """ Point part_1 to parent part """ + + initials = {} + + part_id = self.request.GET.get('part', None) + + if part_id: + try: + initials['part_1'] = Part.objects.get(pk=part_id) + except (Part.DoesNotExist, ValueError): + pass + + return initials + + def get_form(self): + """ Create a form to upload a new PartRelated + + - Hide the 'part_1' field (parent part) + """ + + form = super(AjaxCreateView, self).get_form() + + form.fields['part_1'].widget = HiddenInput() + + return form + + def post_save(self): + """ Save PartRelated model (POST method does not) """ + + form = self.get_form() + + if form.is_valid(): + print('form is valid!') + + part_1 = form.cleaned_data['part_1'] + part_2 = form.cleaned_data['part_2'] + + print(f'{part_1=}') + print(f'{part_2=}') + + PartRelated.create(part_1, part_2) + + +class PartRelatedDelete(AjaxDeleteView): + """ View for deleting a PartRelated object """ + + model = PartRelated + ajax_form_title = _("Delete Related Part") + ajax_template_name = "related_delete.html" + context_object_name = "related" + role_required = 'part.change' + + class PartAttachmentCreate(AjaxCreateView): """ View for creating a new PartAttachment object diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index e5fafef070..fbe1c945e0 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -163,6 +163,41 @@ function loadSimplePartTable(table, url, options={}) { } +function loadPartRelatedTable(table, options={}) { + /* Load related parts table */ + + var columns = [ + { + field: options.params['part'], + title: '{% trans 'Part' %}', + sortable: true, + sortName: 'name', + formatter: function(value, row, index, field) { + + var name = ''; + + if (row.IPN) { + name += row.IPN + ' | ' + row.name; + } else { + name += row.name; + } + + return renderLink(name, '/part/' + row.pk + '/'); + } + } + ]; + + $(table).inventreeTable({ + sortName: 'part', + groupBy: false, + name: options.name || 'related_parts', + formatNoMatches: function() { return "{% trans "No parts found" %}"; }, + columns: columns, + showColumns: true, + }); +} + + function loadParametricPartTable(table, options={}) { /* Load parametric table for part parameters * From 34e4409e7f0521313ce3f5b29a47dbda27ca0edd Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 16 Oct 2020 13:50:31 -0500 Subject: [PATCH 3/5] Functional checkpoint: add/delete related parts from template --- InvenTree/part/models.py | 30 +++++++---- InvenTree/part/templates/part/related.html | 59 ++++++++++++++++------ InvenTree/part/templates/part/tabs.html | 2 +- InvenTree/part/views.py | 28 ++++++---- InvenTree/templates/js/part.html | 35 ------------- 5 files changed, 83 insertions(+), 71 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index aeef72da70..bf979bfbb5 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1254,25 +1254,32 @@ class Part(MPTTModel): return self.get_descendants(include_self=False) - def fetch_related_parts(self): - """ Return all related parts """ + 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 = [] - parts_1 = self.related_parts_1.filter(part_1__id=self.pk) + related_parts_1 = self.related_parts_1.filter(part_1__id=self.pk) - parts_2 = self.related_parts_2.filter(part_2__id=self.pk) + related_parts_2 = self.related_parts_2.filter(part_2__id=self.pk) - for part in parts_1: - # Append - related_parts.append(part.part_2) + for related_part in related_parts_1: + # Add to related parts list + related_parts.append((related_part, related_part.part_2)) - for part in parts_2: - # Append - related_parts.append(part.part_1) + 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 @@ -1783,7 +1790,8 @@ class PartRelated(models.Model): @classmethod def create(cls, part_1, part_2): - ''' Create PartRelated object ''' + ''' 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 index f9175ed590..8fd167b592 100644 --- a/InvenTree/part/templates/part/related.html +++ b/InvenTree/part/templates/part/related.html @@ -18,7 +18,32 @@ - + + + + + + + + {% for item in part.get_related_parts %} + {% with part_related=item.0 part=item.1 %} + + + + {% endwith %} + {% endfor %} + @@ -27,20 +52,24 @@ {% block js_ready %} {{ block.super }} -loadPartRelatedTable($("#table-related-part"), { - url: "{% url 'api-part-list' %}", - params: { - part: {{ part.id }}, - }, -}); - -$("#add-related-part").click(function() { - launchModalForm("{% url 'part-related-create' %}", { - data: { - part: {{ part.id }}, - }, - reload: true, + $('#table-related-part').inventreeTable({ + }); + + $("#add-related-part").click(function() { + launchModalForm("{% url 'part-related-create' %}", { + data: { + part: {{ part.id }}, + }, + reload: true, + }); + }); + + $('.delete-related-part').click(function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + reload: true, + }); }); -}); {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index c96de9b825..8bfaba4d89 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -64,7 +64,7 @@ {% endif %} - {% trans "Related Parts" %} {% if part.related_count > 0 %}{{ part.related_count }}{% endif %} + {% trans "Related" %} {% if part.related_count > 0 %}{{ part.related_count }}{% endif %} {% trans "Attachments" %} {% if part.attachment_count > 0 %}{{ part.attachment_count }}{% endif %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index f49d57bbe3..6ae782ab2c 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -81,10 +81,8 @@ class PartRelatedCreate(AjaxCreateView): ajax_template_name = "modal_form.html" role_required = 'part.change' - # TODO: QuerySet should not show parts already related to object - def get_initial(self): - """ Point part_1 to parent part """ + """ Set parent part as part_1 field """ initials = {} @@ -102,12 +100,30 @@ class PartRelatedCreate(AjaxCreateView): """ Create a form to upload a new PartRelated - Hide the 'part_1' field (parent part) + - Display parts which are not yet related """ form = super(AjaxCreateView, self).get_form() form.fields['part_1'].widget = HiddenInput() + try: + # Get parent part + parent_part = self.get_initial()['part_1'] + # Get existing related parts + related_parts = [related_part[1].pk for related_part in parent_part.get_related_parts()] + + # Build updated choice list excluding parts already related to parent part + updated_choices = [] + for choice in form.fields["part_2"].choices: + if choice[0] not in related_parts: + updated_choices.append(choice) + + # Update choices for related part + form.fields['part_2'].choices = updated_choices + except KeyError: + pass + return form def post_save(self): @@ -116,14 +132,9 @@ class PartRelatedCreate(AjaxCreateView): form = self.get_form() if form.is_valid(): - print('form is valid!') - part_1 = form.cleaned_data['part_1'] part_2 = form.cleaned_data['part_2'] - print(f'{part_1=}') - print(f'{part_2=}') - PartRelated.create(part_1, part_2) @@ -132,7 +143,6 @@ class PartRelatedDelete(AjaxDeleteView): model = PartRelated ajax_form_title = _("Delete Related Part") - ajax_template_name = "related_delete.html" context_object_name = "related" role_required = 'part.change' diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index fbe1c945e0..e5fafef070 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -163,41 +163,6 @@ function loadSimplePartTable(table, url, options={}) { } -function loadPartRelatedTable(table, options={}) { - /* Load related parts table */ - - var columns = [ - { - field: options.params['part'], - title: '{% trans 'Part' %}', - sortable: true, - sortName: 'name', - formatter: function(value, row, index, field) { - - var name = ''; - - if (row.IPN) { - name += row.IPN + ' | ' + row.name; - } else { - name += row.name; - } - - return renderLink(name, '/part/' + row.pk + '/'); - } - } - ]; - - $(table).inventreeTable({ - sortName: 'part', - groupBy: false, - name: options.name || 'related_parts', - formatNoMatches: function() { return "{% trans "No parts found" %}"; }, - columns: columns, - showColumns: true, - }); -} - - function loadParametricPartTable(table, options={}) { /* Load parametric table for part parameters * From 0b26d68d0feca4368aebd4699c367ac8e6947461 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 16 Oct 2020 15:29:58 -0500 Subject: [PATCH 4/5] Added admin view, improved validation of part related relationship --- InvenTree/part/admin.py | 8 ++++- InvenTree/part/models.py | 36 +++++++++++++++++----- InvenTree/part/templates/part/related.html | 2 ++ InvenTree/part/test_views.py | 23 ++++++++++++++ InvenTree/part/views.py | 6 ++-- InvenTree/users/models.py | 1 + 6 files changed, 66 insertions(+), 10 deletions(-) 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/models.py b/InvenTree/part/models.py index bf979bfbb5..db0be1952f 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1754,16 +1754,18 @@ class BomItem(models.Model): 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_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=_('Choose Related Part')) + 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}' + return f'{self.part_1} <--> {self.part_2}' - def create_relationship(self, part_1, part_2): - ''' Create relationship between two parts ''' + def validate(self, part_1, part_2): + ''' Validate that the two parts relationship is unique ''' validate = True @@ -1771,15 +1773,35 @@ class PartRelated(models.Model): 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 is not part_2): + 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 diff --git a/InvenTree/part/templates/part/related.html b/InvenTree/part/templates/part/related.html index 8fd167b592..8c5cc074c8 100644 --- a/InvenTree/part/templates/part/related.html +++ b/InvenTree/part/templates/part/related.html @@ -11,10 +11,12 @@
+ {% if roles.part.change %} + {% endif %}
diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index d8c345d243..1ad5c33e45 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -201,6 +201,29 @@ class PartTests(PartViewTestCase): self.assertEqual(response.status_code, 200) +class PartRelatedTests(PartViewTestCase): + + def test_valid_create(self): + """ test creation of an attachment for a valid part """ + + response = self.client.get(reverse('part-related-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # TODO - Create a new attachment using this view + + def test_invalid_create(self): + """ test creation of an attachment for an invalid part """ + + # TODO + pass + + def test_edit(self): + """ test editing an attachment """ + + # TODO + pass + + class PartAttachmentTests(PartViewTestCase): def test_valid_create(self): diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 6ae782ab2c..b4d8839e13 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -113,10 +113,12 @@ class PartRelatedCreate(AjaxCreateView): # Get existing related parts related_parts = [related_part[1].pk for related_part in parent_part.get_related_parts()] - # Build updated choice list excluding parts already related to parent part + # Build updated choice list excluding + # - parts already related to parent part + # - the parent part itself updated_choices = [] for choice in form.fields["part_2"].choices: - if choice[0] not in related_parts: + if (choice[0] not in related_parts) and (choice[0] != parent_part.pk): updated_choices.append(choice) # Update choices for related part diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index d3c713d07d..d1a3a3b084 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -57,6 +57,7 @@ class RuleSet(models.Model): 'part_parttesttemplate', 'part_partparametertemplate', 'part_partparameter', + 'part_partrelated', ], 'stock': [ 'stock_stockitem', From 5a6cac43f5d734d5948bc6b2069541d03d0fd156 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 16 Oct 2020 15:42:23 -0500 Subject: [PATCH 5/5] Updated migration --- InvenTree/part/migrations/0052_partrelated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/migrations/0052_partrelated.py b/InvenTree/part/migrations/0052_partrelated.py index c84566b3ea..a8672ba7dc 100644 --- a/InvenTree/part/migrations/0052_partrelated.py +++ b/InvenTree/part/migrations/0052_partrelated.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.7 on 2020-10-15 18:51 +# Generated by Django 3.0.7 on 2020-10-16 20:42 from django.db import migrations, models import django.db.models.deletion @@ -16,7 +16,7 @@ class Migration(migrations.Migration): 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(on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_2', 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')), ], ), ]