mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 21:25:42 +00:00 
			
		
		
		
	| @@ -9,7 +9,7 @@ from import_export.fields import Field | |||||||
| import import_export.widgets as widgets | import import_export.widgets as widgets | ||||||
|  |  | ||||||
| from .models import PartCategory, Part | from .models import PartCategory, Part | ||||||
| from .models import PartAttachment, PartStar | from .models import PartAttachment, PartStar, PartRelated | ||||||
| from .models import BomItem | from .models import BomItem | ||||||
| from .models import PartParameterTemplate, PartParameter | from .models import PartParameterTemplate, PartParameter | ||||||
| from .models import PartTestTemplate | from .models import PartTestTemplate | ||||||
| @@ -121,6 +121,11 @@ class PartCategoryAdmin(ImportExportModelAdmin): | |||||||
|     search_fields = ('name', 'description') |     search_fields = ('name', 'description') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PartRelatedAdmin(admin.ModelAdmin): | ||||||
|  |     ''' Class to manage PartRelated objects ''' | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class PartAttachmentAdmin(admin.ModelAdmin): | class PartAttachmentAdmin(admin.ModelAdmin): | ||||||
|  |  | ||||||
|     list_display = ('part', 'attachment', 'comment') |     list_display = ('part', 'attachment', 'comment') | ||||||
| @@ -279,6 +284,7 @@ class PartSellPriceBreakAdmin(admin.ModelAdmin): | |||||||
|  |  | ||||||
| admin.site.register(Part, PartAdmin) | admin.site.register(Part, PartAdmin) | ||||||
| admin.site.register(PartCategory, PartCategoryAdmin) | admin.site.register(PartCategory, PartCategoryAdmin) | ||||||
|  | admin.site.register(PartRelated, PartRelatedAdmin) | ||||||
| admin.site.register(PartAttachment, PartAttachmentAdmin) | admin.site.register(PartAttachment, PartAttachmentAdmin) | ||||||
| admin.site.register(PartStar, PartStarAdmin) | admin.site.register(PartStar, PartStarAdmin) | ||||||
| admin.site.register(BomItem, BomItemAdmin) | admin.site.register(BomItem, BomItemAdmin) | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ from mptt.fields import TreeNodeChoiceField | |||||||
| from django import forms | from django import forms | ||||||
| from django.utils.translation import ugettext as _ | 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 BomItem | ||||||
| from .models import PartParameterTemplate, PartParameter | from .models import PartParameterTemplate, PartParameter | ||||||
| from .models import PartTestTemplate | 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): | class EditPartAttachmentForm(HelperForm): | ||||||
|     """ Form for editing a PartAttachment object """ |     """ Form for editing a PartAttachment object """ | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								InvenTree/part/migrations/0052_partrelated.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								InvenTree/part/migrations/0052_partrelated.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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')), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -1313,6 +1313,32 @@ class Part(MPTTModel): | |||||||
|  |  | ||||||
|         return self.get_descendants(include_self=False) |         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): | def attach_file(instance, filename): | ||||||
|     """ Function for storing a file for a PartAttachment |     """ Function for storing a file for a PartAttachment | ||||||
| @@ -1782,3 +1808,71 @@ class BomItem(models.Model): | |||||||
|         pmax = decimal2string(pmax) |         pmax = decimal2string(pmax) | ||||||
|  |  | ||||||
|         return "{pmin} to {pmax}".format(pmin=pmin, pmax=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
									
								
								InvenTree/part/templates/part/related.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								InvenTree/part/templates/part/related.html
									
									
									
									
									
										Normal file
									
								
							| @@ -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 %} | ||||||
| @@ -63,6 +63,9 @@ | |||||||
|         </a> |         </a> | ||||||
|     </li> |     </li> | ||||||
|     {% endif %} |     {% 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 %}> |     <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> |         <a href="{% url 'part-attachments' part.id %}">{% trans "Attachments" %} {% if part.attachment_count > 0 %}<span class="badge">{{ part.attachment_count }}</span>{% endif %}</a> | ||||||
|     </li> |     </li> | ||||||
|   | |||||||
| @@ -201,6 +201,29 @@ class PartTests(PartViewTestCase): | |||||||
|         self.assertEqual(response.status_code, 200) |         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): | class PartAttachmentTests(PartViewTestCase): | ||||||
|  |  | ||||||
|     def test_valid_create(self): |     def test_valid_create(self): | ||||||
|   | |||||||
| @@ -12,6 +12,11 @@ from django.conf.urls import url, include | |||||||
|  |  | ||||||
| from . import views | 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 = [ | part_attachment_urls = [ | ||||||
|     url(r'^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'), |     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'), |     url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'), | ||||||
| @@ -61,6 +66,7 @@ part_detail_urls = [ | |||||||
|     url(r'^sale-prices/', views.PartDetail.as_view(template_name='part/sale_prices.html'), name='part-sale-prices'), |     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'^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'^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'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), | ||||||
|     url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'), |     url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'), | ||||||
|      |      | ||||||
| @@ -113,6 +119,9 @@ part_urls = [ | |||||||
|     # Part category |     # Part category | ||||||
|     url(r'^category/(?P<pk>\d+)/', include(part_category_urls)), |     url(r'^category/(?P<pk>\d+)/', include(part_category_urls)), | ||||||
|  |  | ||||||
|  |     # Part related | ||||||
|  |     url(r'^related-parts/', include(part_related_urls)), | ||||||
|  |  | ||||||
|     # Part attachments |     # Part attachments | ||||||
|     url(r'^attachment/', include(part_attachment_urls)), |     url(r'^attachment/', include(part_attachment_urls)), | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ import os | |||||||
| from rapidfuzz import fuzz | from rapidfuzz import fuzz | ||||||
| from decimal import Decimal, InvalidOperation | 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 PartParameterTemplate, PartParameter | ||||||
| from .models import BomItem | from .models import BomItem | ||||||
| from .models import match_part_names | from .models import match_part_names | ||||||
| @@ -70,6 +70,85 @@ class PartIndex(InvenTreeRoleMixin, ListView): | |||||||
|         return context |         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' | ||||||
|  |  | ||||||
|  |     def get_initial(self): | ||||||
|  |         """ Set parent part as part_1 field """ | ||||||
|  |  | ||||||
|  |         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) | ||||||
|  |         - 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 | ||||||
|  |             # - the parent part itself | ||||||
|  |             updated_choices = [] | ||||||
|  |             for choice in form.fields["part_2"].choices: | ||||||
|  |                 if (choice[0] not in related_parts) and (choice[0] != parent_part.pk): | ||||||
|  |                     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): | ||||||
|  |         """ Save PartRelated model (POST method does not) """ | ||||||
|  |  | ||||||
|  |         form = self.get_form() | ||||||
|  |  | ||||||
|  |         if form.is_valid(): | ||||||
|  |             part_1 = form.cleaned_data['part_1'] | ||||||
|  |             part_2 = form.cleaned_data['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") | ||||||
|  |     context_object_name = "related" | ||||||
|  |     role_required = 'part.change' | ||||||
|  |  | ||||||
|  |  | ||||||
| class PartAttachmentCreate(AjaxCreateView): | class PartAttachmentCreate(AjaxCreateView): | ||||||
|     """ View for creating a new PartAttachment object |     """ View for creating a new PartAttachment object | ||||||
|  |  | ||||||
|   | |||||||
| @@ -57,6 +57,7 @@ class RuleSet(models.Model): | |||||||
|             'part_parttesttemplate', |             'part_parttesttemplate', | ||||||
|             'part_partparametertemplate', |             'part_partparametertemplate', | ||||||
|             'part_partparameter', |             'part_partparameter', | ||||||
|  |             'part_partrelated', | ||||||
|         ], |         ], | ||||||
|         'stock': [ |         'stock': [ | ||||||
|             'stock_stockitem', |             'stock_stockitem', | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user