Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Related Parts #1048

Merged
merged 5 commits into from Nov 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion InvenTree/part/admin.py
Expand Up @@ -9,7 +9,7 @@
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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -279,6 +284,7 @@ class Meta:

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)
Expand Down
21 changes: 20 additions & 1 deletion InvenTree/part/forms.py
Expand Up @@ -13,7 +13,7 @@
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
Expand Down Expand Up @@ -104,6 +104,25 @@ class Meta:
]


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 """

Expand Down
22 changes: 22 additions & 0 deletions 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')),
],
),
]
94 changes: 94 additions & 0 deletions InvenTree/part/models.py
Expand Up @@ -1254,6 +1254,32 @@ def get_all_variants(self):

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
Expand Down Expand Up @@ -1723,3 +1749,71 @@ def price_range(self):
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
77 changes: 77 additions & 0 deletions 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' %}

<h4>{% trans "Related Parts" %}</h4>
<hr>

<div id='button-bar'>
<div class='button-toolbar container-fluid' style='float: left;'>
{% if roles.part.change %}
<button class='btn btn-primary' type='button' id='add-related-part' title='{% trans "Add Related" %}'>{% trans "Add Related" %}</button>
<div class='filter-list' id='filter-list-related'>
<!-- An empty div in which the filter list will be constructed -->
</div>
{% endif %}
</div>
</div>

<table id='table-related-part' class='table table-condensed table-striped' data-toolbar='#button-toolbar'>
<thead>
<tr>
<th data-field='part' data-serachable='true'>{% trans "Part" %}</th>
</tr>
</thead>
<tbody>
{% for item in part.get_related_parts %}
{% with part_related=item.0 part=item.1 %}
<tr>
<td>
<a class='hover-icon'>
<img class='hover-img-thumb' src='{{ part.get_thumbnail_url }}'>
<img class='hover-img-large' src='{{ part.get_thumbnail_url }}'>
</a>
<a href='/part/{{ part.id }}/'>{{ part }}</a>
<div class='btn-group' style='float: right;'>
{% if roles.part.change %}
<button title='{% trans "Delete" %}' class='btn btn-default btn-glyph delete-related-part' url="{% url 'part-related-delete' part_related.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
{% endif %}
</div>
</td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>


{% endblock %}

{% block js_ready %}
{{ block.super }}

$('#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 %}
3 changes: 3 additions & 0 deletions InvenTree/part/templates/part/tabs.html
Expand Up @@ -63,6 +63,9 @@
</a>
</li>
{% endif %}
<li{% ifequal tab 'related-parts' %} class="active"{% endifequal %}>
<a href="{% url 'part-related' part.id %}">{% trans "Related" %} {% if part.related_count > 0 %}<span class="badge">{{ part.related_count }}</span>{% endif %}</a>
</li>
<li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
<a href="{% url 'part-attachments' part.id %}">{% trans "Attachments" %} {% if part.attachment_count > 0 %}<span class="badge">{{ part.attachment_count }}</span>{% endif %}</a>
</li>
Expand Down
23 changes: 23 additions & 0 deletions InvenTree/part/test_views.py
Expand Up @@ -201,6 +201,29 @@ def test_make_variant(self):
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):
Expand Down
9 changes: 9 additions & 0 deletions InvenTree/part/urls.py
Expand Up @@ -12,6 +12,11 @@

from . import views

part_related_urls = [
url(r'^new/?', views.PartRelatedCreate.as_view(), name='part-related-create'),
url(r'^(?P<pk>\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<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
Expand Down Expand Up @@ -60,6 +65,7 @@
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'),

Expand Down Expand Up @@ -112,6 +118,9 @@
# Part category
url(r'^category/(?P<pk>\d+)/', include(part_category_urls)),

# Part related
url(r'^related-parts/', include(part_related_urls)),

# Part attachments
url(r'^attachment/', include(part_attachment_urls)),

Expand Down