diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 04d1978745..18267f33c4 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -177,7 +177,10 @@ class AjaxCreateView(AjaxMixin, CreateView): # Return the PK of the newly-created object data['pk'] = obj.pk - data['url'] = obj.get_absolute_url() + try: + data['url'] = obj.get_absolute_url() + except AttributeError: + pass return self.renderJsonResponse(request, form, data) @@ -223,7 +226,11 @@ class AjaxUpdateView(AjaxMixin, UpdateView): # Include context data about the updated object data['pk'] = obj.id - data['url'] = obj.get_absolute_url() + + try: + data['url'] = obj.get_absolute_url() + except AttributeError: + pass return self.renderJsonResponse(request, form, data) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index bba8c3e973..0363b4ab37 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -9,7 +9,8 @@ from InvenTree.forms import HelperForm from django import forms -from .models import Part, PartCategory, BomItem +from .models import Part, PartCategory, PartAttachment +from .models import BomItem from .models import SupplierPart @@ -44,6 +45,18 @@ class BomExportForm(HelperForm): ] +class EditPartAttachmentForm(HelperForm): + """ Form for editing a PartAttachment object """ + + class Meta: + model = PartAttachment + fields = [ + 'part', + 'attachment', + 'comment' + ] + + class EditPartForm(HelperForm): """ Form for editing a Part object """ diff --git a/InvenTree/part/migrations/0012_part_active.py b/InvenTree/part/migrations/0012_part_active.py index c2f3b55f6c..87929e2f85 100644 --- a/InvenTree/part/migrations/0012_part_active.py +++ b/InvenTree/part/migrations/0012_part_active.py @@ -14,5 +14,10 @@ class Migration(migrations.Migration): model_name='part', name='active', field=models.BooleanField(default=True, help_text='Is this part active?'), + ), + migrations.AddField( + model_name='partattachment', + name='comment', + field=models.CharField(blank=True, help_text='File comment', max_length=100), ), ] diff --git a/InvenTree/part/migrations/0014_partattachment_comment.py b/InvenTree/part/migrations/0014_partattachment_comment.py deleted file mode 100644 index a51f588a59..0000000000 --- a/InvenTree/part/migrations/0014_partattachment_comment.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2 on 2019-04-30 23:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('part', '0013_auto_20190429_2229'), - ] - - operations = [ - migrations.AddField( - model_name='partattachment', - name='comment', - field=models.CharField(blank=True, help_text='Attachment description', max_length=100), - ), - ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 19682929cf..ab83b584cd 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -125,7 +125,7 @@ class Part(models.Model): IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number') # Provide a URL for an external link - URL = models.URLField(blank=True, help_text='Link to external URL') + URL = models.URLField(blank=True, help_text='Link to extenal URL') # Part category - all parts must be assigned to a category category = models.ForeignKey(PartCategory, related_name='parts', @@ -307,12 +307,6 @@ class Part(models.Model): def used_in_count(self): return self.used_in.count() - def required_parts(self): - parts = [] - for bom in self.bom_items.all(): - parts.append(bom.sub_part) - return parts - @property def supplier_count(self): # Return the number of supplier parts available for this part @@ -366,7 +360,7 @@ class PartAttachment(models.Model): attachment = models.FileField(upload_to=attach_file, null=True, blank=True) - comment = models.CharField(max_length=100, blank=True, help_text="Attachment description") + comment = models.CharField(max_length=100, blank=True, help_text='File comment') @property def basename(self): @@ -407,11 +401,8 @@ class BomItem(models.Model): - A part cannot refer to a part which refers to it """ - if self.part is None or self.sub_part is None: - # Field validation will catch these None values - pass # A part cannot refer to itself in its BOM - elif self.part == self.sub_part: + if self.part == self.sub_part: raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')}) # Test for simple recursion diff --git a/InvenTree/part/templates/part/attachment_delete.html b/InvenTree/part/templates/part/attachment_delete.html new file mode 100644 index 0000000000..db98b7f6d6 --- /dev/null +++ b/InvenTree/part/templates/part/attachment_delete.html @@ -0,0 +1,3 @@ +Are you sure you wish to delete this attachment? +
+This will remove the file '{{ attachment.basename }}'. \ No newline at end of file diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html new file mode 100644 index 0000000000..24c69f23f7 --- /dev/null +++ b/InvenTree/part/templates/part/attachments.html @@ -0,0 +1,62 @@ +{% extends "part/part_base.html" %} +{% load static %} + +{% block details %} + +{% include 'part/tabs.html' with tab='attachments' %} + +

Attachments

+ +
+ +
+ + + + + + + +{% for attachment in part.attachments.all %} + + + + + +{% endfor %} +
FileComment
{{ attachment.basename }}{{ attachment.comment }} +
+ + +
+
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + + $("#new-attachment").click(function() { + launchModalForm("{% url 'part-attachment-create' %}?part={{ part.id }}"); + }); + + $("#attachment-table").on('click', '.attachment-edit-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), + { + success: function() { + } + }); + }); + + $("#attachment-table").on('click', '.attachment-delete-button', function() { + var button = $(this); + + launchDeleteForm(button.attr('url'), { + success: function() { + } + }); + }); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 79be275307..a1b4ddc2d1 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -66,13 +66,6 @@ {% endif %} - -

Attachments

- diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index aa9d798bb7..2bcf894b2a 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -1,6 +1,15 @@ \ No newline at end of file diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 96c830f556..401a038193 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -1,5 +1,11 @@ """ -URL lookup for Part app +URL lookup for Part app. Provides URL endpoints for: + +- Display / Create / Edit / Delete PartCategory +- Display / Create / Edit / Delete Part +- Create / Edit / Delete PartAttachment +- Display / Create / Edit / Delete SupplierPart + """ from django.conf.urls import url, include @@ -19,11 +25,18 @@ supplier_part_urls = [ url(r'^(?P\d+)/', include(supplier_part_detail_urls)), ] +part_attachment_urls = [ + url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'), + url(r'^(?P\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'), + url(r'^(?P\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), +] + part_detail_urls = [ url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), + url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'), url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'), url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), @@ -69,6 +82,10 @@ part_urls = [ # Part category url(r'^category/(?P\d+)/', include(part_category_urls)), + # Part attachments + url(r'^attachment/', include(part_attachment_urls)), + + # Bom Items url(r'^bom/(?P\d+)/', include(part_bom_urls)), # Top level part list (display top level parts and categories) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 7ea8e5efbf..bdc3147e9b 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -13,11 +13,13 @@ from django.forms.models import model_to_dict from django.forms import HiddenInput from company.models import Company -from .models import PartCategory, Part, BomItem +from .models import PartCategory, Part, PartAttachment +from .models import BomItem from .models import SupplierPart from .forms import PartImageForm from .forms import EditPartForm +from .forms import EditPartAttachmentForm from .forms import EditCategoryForm from .forms import EditBomItemForm from .forms import BomExportForm @@ -51,6 +53,81 @@ class PartIndex(ListView): return context +class PartAttachmentCreate(AjaxCreateView): + """ View for creating a new PartAttachment object + + - The view only makes sense if a Part object is passed to it + """ + model = PartAttachment + form_class = EditPartAttachmentForm + ajax_form_title = "Add part attachment" + ajax_template_name = "modal_form.html" + + def get_data(self): + return { + 'success': 'Added attachment' + } + + def get_initial(self): + """ Get initial data for new PartAttachment object. + + - Client should have requested this form with a parent part in mind + - e.g. ?part= + """ + + initials = super(AjaxCreateView, self).get_initial() + + # TODO - If the proper part was not sent, return an error message + initials['part'] = Part.objects.get(id=self.request.GET.get('part')) + + return initials + + def get_form(self): + """ Create a form to upload a new PartAttachment + + - Hide the 'part' field + """ + + form = super(AjaxCreateView, self).get_form() + + form.fields['part'].widget = HiddenInput() + + return form + + +class PartAttachmentEdit(AjaxUpdateView): + """ View for editing a PartAttachment object """ + model = PartAttachment + form_class = EditPartAttachmentForm + ajax_template_name = 'modal_form.html' + ajax_form_title = 'Edit attachment' + + def get_data(self): + return { + 'success': 'Part attachment updated' + } + + def get_form(self): + form = super(AjaxUpdateView, self).get_form() + + form.fields['part'].widget = HiddenInput() + + return form + + +class PartAttachmentDelete(AjaxDeleteView): + """ View for deleting a PartAttachment """ + + model = PartAttachment + ajax_template_name = "part/attachment_delete.html" + context_object_name = "attachment" + + def get_data(self): + return { + 'danger': 'Deleted part attachment' + } + + class PartCreate(AjaxCreateView): """ View for creating a new Part object. @@ -102,7 +179,6 @@ class PartCreate(AjaxCreateView): return form - # Pre-fill the category field if a valid category is provided def get_initial(self): """ Get initial data for the new Part object: diff --git a/setup.cfg b/setup.cfg index f6f50b0c03..697f6d1f02 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,5 +4,5 @@ ignore = W293, # - E501 - line too long (82 characters) E501 -exclude = .git,__pycache__ +exclude = .git,__pycache__,*/migrations/* max-complexity = 20