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 *