From 64f6238351f16458b6e1e96647ebb8f8a1c35e4f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 May 2020 09:39:28 +1000 Subject: [PATCH 1/4] Add StockItemAttachment model --- InvenTree/stock/admin.py | 9 ++++++- .../migrations/0036_stockitemattachment.py | 27 +++++++++++++++++++ InvenTree/stock/models.py | 17 +++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 InvenTree/stock/migrations/0036_stockitemattachment.py diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index e233908ec7..a9cbcc9de7 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -8,7 +8,7 @@ from import_export.resources import ModelResource from import_export.fields import Field import import_export.widgets as widgets -from .models import StockLocation, StockItem +from .models import StockLocation, StockItem, StockItemAttachment from .models import StockItemTracking from build.models import Build @@ -108,6 +108,12 @@ class StockItemAdmin(ImportExportModelAdmin): list_display = ('part', 'quantity', 'location', 'status', 'updated') +class StockAttachmentAdmin(admin.ModelAdmin): + + list_display = ('stock_item', 'attachment', 'comment') + + + class StockTrackingAdmin(ImportExportModelAdmin): list_display = ('item', 'date', 'title') @@ -115,3 +121,4 @@ class StockTrackingAdmin(ImportExportModelAdmin): admin.site.register(StockLocation, LocationAdmin) admin.site.register(StockItem, StockItemAdmin) admin.site.register(StockItemTracking, StockTrackingAdmin) +admin.site.register(StockItemAttachment, StockAttachmentAdmin) diff --git a/InvenTree/stock/migrations/0036_stockitemattachment.py b/InvenTree/stock/migrations/0036_stockitemattachment.py new file mode 100644 index 0000000000..946f6251b0 --- /dev/null +++ b/InvenTree/stock/migrations/0036_stockitemattachment.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.5 on 2020-05-06 23:36 + +import InvenTree.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0035_auto_20200502_2308'), + ] + + operations = [ + migrations.CreateModel( + name='StockItemAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attachment', models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment)), + ('comment', models.CharField(help_text='File comment', max_length=100)), + ('stock_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='stock.StockItem')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index f514eea3da..6ab8ec07ee 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -27,7 +27,7 @@ from datetime import datetime from InvenTree import helpers from InvenTree.status_codes import StockStatus -from InvenTree.models import InvenTreeTree +from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeURLField from part import models as PartModels @@ -935,6 +935,21 @@ def before_delete_stock_item(sender, instance, using, **kwargs): StockItem.objects.rebuild() +class StockItemAttachment(InvenTreeAttachment): + """ + Model for storing file attachments against a StockItem object. + """ + + def getSubdir(self): + return os.path.join("stock_files", str(self.stock_item.id)) + + stock_item = models.ForeignKey( + StockItem, + on_delete=models.CASCADE, + related_name='attachments' + ) + + class StockItemTracking(models.Model): """ Stock tracking entry - breacrumb for keeping track of automated stock transactions From 14132a6efac478f66ad071b13b2ffa4d67a243e8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 May 2020 09:57:54 +1000 Subject: [PATCH 2/4] Add views / models / etc etc to support StockItem attachment --- InvenTree/part/views.py | 2 +- InvenTree/stock/forms.py | 16 +++- InvenTree/stock/models.py | 2 + .../templates/stock}/attachment_delete.html | 0 .../templates/stock/item_attachments.html | 82 +++++++++++++++++++ InvenTree/stock/templates/stock/tabs.html | 3 + InvenTree/stock/urls.py | 7 ++ InvenTree/stock/views.py | 75 ++++++++++++++++- InvenTree/templates/attachment_delete.html | 7 ++ 9 files changed, 191 insertions(+), 3 deletions(-) rename InvenTree/{part/templates/part => stock/templates/stock}/attachment_delete.html (100%) create mode 100644 InvenTree/stock/templates/stock/item_attachments.html create mode 100644 InvenTree/templates/attachment_delete.html diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 458696b75c..89a285cf60 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -135,7 +135,7 @@ class PartAttachmentDelete(AjaxDeleteView): model = PartAttachment ajax_form_title = _("Delete Part Attachment") - ajax_template_name = "part/attachment_delete.html" + ajax_template_name = "attachment_delete.html" context_object_name = "attachment" def get_data(self): diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index fef6ecc7a5..a4578440cb 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -13,7 +13,21 @@ from mptt.fields import TreeNodeChoiceField from InvenTree.helpers import GetExportFormats from InvenTree.forms import HelperForm -from .models import StockLocation, StockItem, StockItemTracking +from .models import StockLocation, StockItem, StockItemTracking, StockItemAttachment + + +class EditStockItemAttachmentForm(HelperForm): + """ + Form for creating / editing a StockItemAttachment object + """ + + class Meta: + model = StockItemAttachment + fields = [ + 'stock_item', + 'attachment', + 'comment' + ] class EditStockLocationForm(HelperForm): diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 6ab8ec07ee..ca5ae115a1 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -6,6 +6,8 @@ Stock database model definitions # -*- coding: utf-8 -*- from __future__ import unicode_literals +import os + from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError from django.urls import reverse diff --git a/InvenTree/part/templates/part/attachment_delete.html b/InvenTree/stock/templates/stock/attachment_delete.html similarity index 100% rename from InvenTree/part/templates/part/attachment_delete.html rename to InvenTree/stock/templates/stock/attachment_delete.html diff --git a/InvenTree/stock/templates/stock/item_attachments.html b/InvenTree/stock/templates/stock/item_attachments.html new file mode 100644 index 0000000000..6aeff554d0 --- /dev/null +++ b/InvenTree/stock/templates/stock/item_attachments.html @@ -0,0 +1,82 @@ +{% extends "stock/item_base.html" %} + +{% load static %} +{% load i18n %} + +{% block details %} + +{% include "stock/tabs.html" with tab='attachments' %} + +
+

{% trans "Stock Item Attachments" %}

+ + +
+
+ +
+
+ + + + + + + + + + + {% for attachment in item.attachments.all %} + + + + + + {% endfor %} + +
{% trans "File" %}{% trans "Comment" %}
{{ attachment.basename }}{{ attachment.comment }} +
+ + +
+
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +$("#new-attachment").click(function() { + launchModalForm("{% url 'stock-item-attachment-create' %}?item={{ item.id }}", + { + reload: true, + }); +}); + +$("#attachment-table").on('click', '.attachment-edit-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), + { + reload: true, + }); +}); + +$("#attachment-table").on('click', '.attachment-delete-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + success: function() { + location.reload(); + } + }); +}); + +$("#attachment-table").inventreeTable({ +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/tabs.html b/InvenTree/stock/templates/stock/tabs.html index 62ee68bc4b..5b69cac8b4 100644 --- a/InvenTree/stock/templates/stock/tabs.html +++ b/InvenTree/stock/templates/stock/tabs.html @@ -16,4 +16,7 @@ {% trans "Notes" %}{% if item.notes %} {% endif %} + + {% trans "Attachments" %}{% if item.attachments.count > 0 %}{{ item.attachments.count }}{% endif %} + \ No newline at end of file diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index f69dc8b63f..149c0dde2f 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -25,6 +25,7 @@ stock_item_detail_urls = [ url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'), url(r'^children/', views.StockItemDetail.as_view(template_name='stock/item_childs.html'), name='stock-item-children'), + url(r'^attachments/', views.StockItemDetail.as_view(template_name='stock/item_attachments.html'), name='stock-item-attachments'), url(r'^notes/', views.StockItemNotes.as_view(), name='stock-item-notes'), url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'), @@ -50,6 +51,12 @@ stock_urls = [ url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'), + url(r'^item/attachment/', include([ + url(r'^new/', views.StockItemAttachmentCreate.as_view(), name='stock-item-attachment-create'), + url(r'^(?P\d+)/edit/', views.StockItemAttachmentEdit.as_view(), name='stock-item-attachment-edit'), + url(r'^(?P\d+)/delete/', views.StockItemAttachmentDelete.as_view(), name='stock-item-attachment-delete'), + ])), + url(r'^track/', include(stock_tracking_urls)), url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index b6ec8bda85..19edf8666d 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -26,7 +26,7 @@ from datetime import datetime from company.models import Company, SupplierPart from part.models import Part -from .models import StockItem, StockLocation, StockItemTracking +from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment from .admin import StockItemResource @@ -37,6 +37,7 @@ from .forms import AdjustStockForm from .forms import TrackingEntryForm from .forms import SerializeStockForm from .forms import ExportOptionsForm +from .forms import EditStockItemAttachmentForm class StockIndex(ListView): @@ -149,6 +150,78 @@ class StockLocationQRCode(QRCodeView): return None +class StockItemAttachmentCreate(AjaxCreateView): + """ + View for adding a new attachment for a StockItem + """ + + model = StockItemAttachment + form_class = EditStockItemAttachmentForm + ajax_form_title = _("Add Stock Item Attachment") + ajax_template_name = "modal_form.html" + + def get_data(self): + return { + 'success': _("Added attachment") + } + + def get_initial(self): + """ + Get initial data for the new StockItem attachment object. + + - Client must provide a valid StockItem ID + """ + + initials = super().get_initial() + + try: + initials['stock_item'] = StockItem.objects.get(id=self.request.GET.get('item', None)) + except (ValueError, StockItem.DoesNotExist): + pass + + return initials + + def get_form(self): + + form = super().get_form() + form.fields['stock_item'].widget = HiddenInput() + + return form + + +class StockItemAttachmentEdit(AjaxUpdateView): + """ + View for editing a StockItemAttachment object. + """ + + model = StockItemAttachment + form_class = EditStockItemAttachmentForm + ajax_form_title = _("Edit Stock Item Attachment") + + def get_form(self): + + form = super().get_form() + form.fields['stock_item'].widget = HiddenInput() + + return form + + +class StockItemAttachmentDelete(AjaxDeleteView): + """ + View for deleting a StockItemAttachment object. + """ + + model = StockItemAttachment + ajax_form_title = _("Delete Stock Item Attachment") + ajax_template_name = "attachment_delete.html" + context_object_name = "attachment" + + def get_data(self): + return { + 'danger': _("Deleted attachment"), + } + + class StockExportOptions(AjaxView): """ Form for selecting StockExport options """ diff --git a/InvenTree/templates/attachment_delete.html b/InvenTree/templates/attachment_delete.html new file mode 100644 index 0000000000..4ee7f03cb1 --- /dev/null +++ b/InvenTree/templates/attachment_delete.html @@ -0,0 +1,7 @@ +{% extends "modal_delete_form.html" %} +{% load i18n %} + +{% block pre_form_content %} +{% trans "Are you sure you want to delete this attachment?" %} +
+{% endblock %} \ No newline at end of file From ea0d32fd3d3b865e8532519d5e2bc09dc52260e2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 May 2020 09:58:29 +1000 Subject: [PATCH 3/4] PEP fixes --- InvenTree/stock/admin.py | 1 - InvenTree/stock/models.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index a9cbcc9de7..86099cdbac 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -113,7 +113,6 @@ class StockAttachmentAdmin(admin.ModelAdmin): list_display = ('stock_item', 'attachment', 'comment') - class StockTrackingAdmin(ImportExportModelAdmin): list_display = ('item', 'date', 'title') diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index ca5ae115a1..ecfbf751c9 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -949,7 +949,7 @@ class StockItemAttachment(InvenTreeAttachment): StockItem, on_delete=models.CASCADE, related_name='attachments' - ) + ) class StockItemTracking(models.Model): From 003b384abd978729fa6253f18270d7de22d216f3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 May 2020 10:04:16 +1000 Subject: [PATCH 4/4] Slight adjustment to page tabs for StockItem view --- InvenTree/stock/templates/stock/tabs.html | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/InvenTree/stock/templates/stock/tabs.html b/InvenTree/stock/templates/stock/tabs.html index 5b69cac8b4..fddd2f095f 100644 --- a/InvenTree/stock/templates/stock/tabs.html +++ b/InvenTree/stock/templates/stock/tabs.html @@ -1,12 +1,15 @@ {% load i18n %} \ No newline at end of file