diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index abb6102a6f..eb87b8f77a 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -8,6 +8,9 @@ from __future__ import unicode_literals from django.utils.translation import ugettext as _ from django.http import JsonResponse +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters + from rest_framework import permissions from rest_framework.response import Response from rest_framework.views import APIView @@ -41,6 +44,28 @@ class InfoView(AjaxView): return JsonResponse(data) +class AttachmentMixin: + """ + Mixin for creating attachment objects, + and ensuring the user information is saved correctly. + """ + + permission_classes = [permissions.IsAuthenticated] + + filter_backends = [ + DjangoFilterBackend, + filters.OrderingFilter, + filters.SearchFilter, + ] + + def perform_create(self, serializer): + """ Save the user information when a file is uploaded """ + + attachment = serializer.save() + attachment.user = self.request.user + attachment.save() + + class ActionPluginView(APIView): """ Endpoint for running custom action plugins. diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index d2a5c5daa6..e192e34a0f 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals import os from django.db import models +from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ @@ -41,6 +42,8 @@ class InvenTreeAttachment(models.Model): Attributes: attachment: File comment: String descriptor for the attachment + user: User associated with file upload + upload_date: Date the file was uploaded """ def getSubdir(self): """ @@ -55,6 +58,15 @@ class InvenTreeAttachment(models.Model): comment = models.CharField(max_length=100, help_text=_('File comment')) + user = models.ForeignKey( + User, + on_delete=models.SET_NULL, + blank=True, null=True, + help_text=_('User'), + ) + + upload_date = models.DateField(auto_now_add=True, null=True, blank=True) + @property def basename(self): return os.path.basename(self.attachment.name) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index cb48b4c11d..dba493baab 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -12,6 +12,7 @@ from rest_framework import filters from django.conf.urls import url, include from InvenTree.helpers import str2bool +from InvenTree.api import AttachmentMixin from part.models import Part from company.models import SupplierPart @@ -200,7 +201,7 @@ class POLineItemDetail(generics.RetrieveUpdateAPIView): ] -class SOAttachmentList(generics.ListCreateAPIView): +class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ API endpoint for listing (and creating) a SalesOrderAttachment (file upload) """ @@ -208,12 +209,6 @@ class SOAttachmentList(generics.ListCreateAPIView): queryset = SalesOrderAttachment.objects.all() serializer_class = SOAttachmentSerializer - filter_backends = [ - DjangoFilterBackend, - filters.OrderingFilter, - filters.SearchFilter, - ] - filter_fields = [ 'order', ] @@ -399,7 +394,7 @@ class SOLineItemDetail(generics.RetrieveUpdateAPIView): permission_classes = [permissions.IsAuthenticated] -class POAttachmentList(generics.ListCreateAPIView): +class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload) """ @@ -407,12 +402,6 @@ class POAttachmentList(generics.ListCreateAPIView): queryset = PurchaseOrderAttachment.objects.all() serializer_class = POAttachmentSerializer - filter_backends = [ - DjangoFilterBackend, - filters.OrderingFilter, - filters.SearchFilter, - ] - filter_fields = [ 'order', ] diff --git a/InvenTree/order/migrations/0033_auto_20200512_1033.py b/InvenTree/order/migrations/0033_auto_20200512_1033.py new file mode 100644 index 0000000000..2c3abbb0d0 --- /dev/null +++ b/InvenTree/order/migrations/0033_auto_20200512_1033.py @@ -0,0 +1,26 @@ +# Generated by Django 3.0.5 on 2020-05-12 10:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('order', '0032_auto_20200427_0044'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderattachment', + name='user', + field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='salesorderattachment', + name='user', + field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/InvenTree/order/migrations/0034_auto_20200512_1054.py b/InvenTree/order/migrations/0034_auto_20200512_1054.py new file mode 100644 index 0000000000..9124bb1cce --- /dev/null +++ b/InvenTree/order/migrations/0034_auto_20200512_1054.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.5 on 2020-05-12 10:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0033_auto_20200512_1033'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderattachment', + name='upload_date', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='salesorderattachment', + name='upload_date', + field=models.DateField(auto_now_add=True, null=True), + ), + ] diff --git a/InvenTree/order/templates/order/po_attachments.html b/InvenTree/order/templates/order/po_attachments.html index 388b7197b6..a08b618fca 100644 --- a/InvenTree/order/templates/order/po_attachments.html +++ b/InvenTree/order/templates/order/po_attachments.html @@ -12,39 +12,8 @@
-
-
- -
-
+{% include "attachment_table.html" with attachments=order.attachments.all %} - - - - - - - - - - {% for attachment in order.attachments.all %} - - - - - - {% endfor %} - -
{% trans "File" %}{% trans "Comment" %}
{{ attachment.basename }}{{ attachment.comment }} -
- - -
-
{% endblock %} @@ -61,8 +30,10 @@ $("#new-attachment").click(function() { $("#attachment-table").on('click', '.attachment-edit-button', function() { var button = $(this); + + var url = `/order/purchase-order/attachment/${button.attr('pk')}/edit/`; - launchModalForm(button.attr('url'), { + launchModalForm(url, { reload: true, }); }); @@ -70,7 +41,11 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() { var button = $(this); - launchModalForm(button.attr('url'), { + var url = `/order/purchase-order/attachment/${button.attr('pk')}/delete/`; + + console.log("url: " + url); + + launchModalForm(url, { reload: true, }); }); diff --git a/InvenTree/order/templates/order/so_attachments.html b/InvenTree/order/templates/order/so_attachments.html index ca4170beff..aff62213e5 100644 --- a/InvenTree/order/templates/order/so_attachments.html +++ b/InvenTree/order/templates/order/so_attachments.html @@ -8,43 +8,11 @@ {% include 'order/so_tabs.html' with tab='attachments' %} -

{% trans "Sales Order Attachments" %} +

{% trans "Sales Order Attachments" %}


-
-
- -
-
- - - - - - - - - - - {% for attachment in order.attachments.all %} - - - - - - {% endfor %} - -
{% trans "File" %}{% trans "Comment" %}
{{ attachment.basename }}{{ attachment.comment }} -
- - -
-
+{% include "attachment_table.html" with attachments=order.attachments.all %} {% endblock %} @@ -62,7 +30,9 @@ $("#new-attachment").click(function() { $("#attachment-table").on('click', '.attachment-edit-button', function() { var button = $(this); - launchModalForm(button.attr('url'), { + var url = `/order/sales-order/attachment/${button.attr('pk')}/edit/`; + + launchModalForm(url, { reload: true, }); }); @@ -70,7 +40,9 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() { var button = $(this); - launchModalForm(button.attr('url'), { + var url = `/order/sales-order/attachment/${button.attr('pk')}/delete/`; + + launchModalForm(url, { reload: true, }); }); diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index daa83eb5ab..22104df5c7 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -42,7 +42,7 @@ purchase_order_urls = [ ])), ])), - url(r'^attachments/', include([ + url(r'^attachment/', include([ url(r'^new/', views.PurchaseOrderAttachmentCreate.as_view(), name='po-attachment-create'), url(r'^(?P\d+)/edit/', views.PurchaseOrderAttachmentEdit.as_view(), name='po-attachment-edit'), url(r'^(?P\d+)/delete/', views.PurchaseOrderAttachmentDelete.as_view(), name='po-attachment-delete'), @@ -86,7 +86,7 @@ sales_order_urls = [ ])), ])), - url(r'^attachments/', include([ + url(r'^attachment/', include([ url(r'^new/', views.SalesOrderAttachmentCreate.as_view(), name='so-attachment-create'), url(r'^(?P\d+)/edit/', views.SalesOrderAttachmentEdit.as_view(), name='so-attachment-edit'), url(r'^(?P\d+)/delete/', views.SalesOrderAttachmentDelete.as_view(), name='so-attachment-delete'), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 02132d34cc..370c629cac 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -93,6 +93,10 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView): ajax_form_title = _("Add Purchase Order Attachment") ajax_template_name = "modal_form.html" + def post_save(self, **kwargs): + self.object.user = self.request.user + self.object.save() + def get_data(self): return { "success": _("Added attachment") @@ -133,6 +137,10 @@ class SalesOrderAttachmentCreate(AjaxCreateView): form_class = order_forms.EditSalesOrderAttachmentForm ajax_form_title = _('Add Sales Order Attachment') + def post_save(self, **kwargs): + self.object.user = self.request.user + self.object.save() + def get_data(self): return { 'success': _('Added attachment') diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 8f9121885d..84ff83af0a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -25,6 +25,7 @@ from . import serializers as part_serializers from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool, isNull +from InvenTree.api import AttachmentMixin class PartCategoryTree(TreeSerializer): @@ -106,7 +107,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): queryset = PartCategory.objects.all() -class PartAttachmentList(generics.ListCreateAPIView): +class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ API endpoint for listing (and creating) a PartAttachment (file upload). """ @@ -114,14 +115,6 @@ class PartAttachmentList(generics.ListCreateAPIView): queryset = PartAttachment.objects.all() serializer_class = part_serializers.PartAttachmentSerializer - permission_classes = [permissions.IsAuthenticated] - - filter_backends = [ - DjangoFilterBackend, - filters.OrderingFilter, - filters.SearchFilter, - ] - filter_fields = [ 'part', ] @@ -296,24 +289,17 @@ class PartList(generics.ListCreateAPIView): else: return Response(data) - def create(self, request, *args, **kwargs): - """ Override the default 'create' behaviour: + def perform_create(self, serializer): + """ We wish to save the user who created this part! - Note: Implementation coped from DRF class CreateModelMixin + Note: Implementation copied from DRF class CreateModelMixin """ - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - # Record the user who created this Part object part = serializer.save() - part.creation_user = request.user + part.creation_user = self.request.user part.save() - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) diff --git a/InvenTree/part/migrations/0036_partattachment_user.py b/InvenTree/part/migrations/0036_partattachment_user.py new file mode 100644 index 0000000000..d59bc7ffb2 --- /dev/null +++ b/InvenTree/part/migrations/0036_partattachment_user.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.5 on 2020-05-12 10:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('part', '0035_auto_20200406_0045'), + ] + + operations = [ + migrations.AddField( + model_name='partattachment', + name='user', + field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/InvenTree/part/migrations/0037_partattachment_upload_date.py b/InvenTree/part/migrations/0037_partattachment_upload_date.py new file mode 100644 index 0000000000..4169870178 --- /dev/null +++ b/InvenTree/part/migrations/0037_partattachment_upload_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-05-12 10:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0036_partattachment_user'), + ] + + operations = [ + migrations.AddField( + model_name='partattachment', + name='upload_date', + field=models.DateField(auto_now_add=True, null=True), + ), + ] diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html index b89a40e654..965645b748 100644 --- a/InvenTree/part/templates/part/attachments.html +++ b/InvenTree/part/templates/part/attachments.html @@ -9,54 +9,7 @@
-
-
- -
-
- - - - - - - - - - - - {% for attachment in part.attachments.all %} - - - - - - {% endfor %} - -{% if part.variant_of and part.variant_of.attachments.count > 0 %} - - - -{% for attachment in part.variant_of.attachments.all %} - - - - - -{% endfor %} -{% endif %} -
{% trans "File" %}{% trans "Comment" %}
{{ attachment.basename }}{{ attachment.comment }} -
- - -
-
- Attachments for template part {{ part.variant_of.full_name }} -
{{ attachment.basename }}{{ attachment.comment }}
+{% include "attachment_table.html" with attachments=part.attachments.all %} {% endblock %} @@ -73,7 +26,9 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() { var button = $(this); - launchModalForm(button.attr('url'), + var url = `/part/attachment/${button.attr('pk')}/edit/`; + + launchModalForm(url, { reload: true, }); @@ -82,7 +37,9 @@ $("#attachment-table").on('click', '.attachment-delete-button', function() { var button = $(this); - launchModalForm(button.attr('url'), { + var url = `/part/attachment/${button.attr('pk')}/delete/`; + + launchModalForm(url, { success: function() { location.reload(); } diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 89a285cf60..ce2fc415be 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -74,6 +74,11 @@ class PartAttachmentCreate(AjaxCreateView): ajax_form_title = _("Add part attachment") ajax_template_name = "modal_form.html" + def post_save(self): + """ Record the user that uploaded the attachment """ + self.object.user = self.request.user + self.object.save() + def get_data(self): return { 'success': _('Added attachment') diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index f72f2590cc..1c654d8b3c 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -29,6 +29,7 @@ from .serializers import StockItemAttachmentSerializer from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool, isNull +from InvenTree.api import AttachmentMixin from decimal import Decimal, InvalidOperation @@ -478,6 +479,23 @@ class StockList(generics.ListCreateAPIView): if sales_order: queryset = queryset.filter(sales_order=sales_order) + + # Filter by "serialized" status? + serialized = params.get('serialized', None) + + if serialized is not None: + serialized = str2bool(serialized) + + if serialized: + queryset = queryset.exclude(serial=None) + else: + queryset = queryset.filter(serial=None) + + # Filter by serial number? + serial_number = params.get('serial', None) + + if serial_number is not None: + queryset = queryset.filter(serial=serial_number) in_stock = self.request.query_params.get('in_stock', None) @@ -628,7 +646,7 @@ class StockList(generics.ListCreateAPIView): ] -class StockAttachmentList(generics.ListCreateAPIView): +class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ API endpoint for listing (and creating) a StockItemAttachment (file upload) """ @@ -636,12 +654,6 @@ class StockAttachmentList(generics.ListCreateAPIView): queryset = StockItemAttachment.objects.all() serializer_class = StockItemAttachmentSerializer - filter_backends = [ - DjangoFilterBackend, - filters.OrderingFilter, - filters.SearchFilter, - ] - filter_fields = [ 'stock_item', ] diff --git a/InvenTree/stock/migrations/0037_stockitemattachment_user.py b/InvenTree/stock/migrations/0037_stockitemattachment_user.py new file mode 100644 index 0000000000..b0f87df81a --- /dev/null +++ b/InvenTree/stock/migrations/0037_stockitemattachment_user.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.5 on 2020-05-12 10:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('stock', '0036_stockitemattachment'), + ] + + operations = [ + migrations.AddField( + model_name='stockitemattachment', + name='user', + field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/InvenTree/stock/migrations/0038_stockitemattachment_upload_date.py b/InvenTree/stock/migrations/0038_stockitemattachment_upload_date.py new file mode 100644 index 0000000000..0347b6f0ea --- /dev/null +++ b/InvenTree/stock/migrations/0038_stockitemattachment_upload_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-05-12 10:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0037_stockitemattachment_user'), + ] + + operations = [ + migrations.AddField( + model_name='stockitemattachment', + name='upload_date', + field=models.DateField(auto_now_add=True, null=True), + ), + ] diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 69cb726f73..622f93e620 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -193,6 +193,16 @@ class LocationSerializer(InvenTreeModelSerializer): class StockItemAttachmentSerializer(InvenTreeModelSerializer): """ Serializer for StockItemAttachment model """ + def __init_(self, *args, **kwargs): + user_detail = kwargs.pop('user_detail', False) + + super().__init__(*args, **kwargs) + + if user_detail is not True: + self.fields.pop('user_detail') + + user_detail = UserSerializerBrief(source='user', read_only=True) + class Meta: model = StockItemAttachment @@ -200,7 +210,9 @@ class StockItemAttachmentSerializer(InvenTreeModelSerializer): 'pk', 'stock_item', 'attachment', - 'comment' + 'comment', + 'user', + 'user_detail', ] diff --git a/InvenTree/stock/templates/stock/item_attachments.html b/InvenTree/stock/templates/stock/item_attachments.html index 6aeff554d0..2d056afbcf 100644 --- a/InvenTree/stock/templates/stock/item_attachments.html +++ b/InvenTree/stock/templates/stock/item_attachments.html @@ -10,40 +10,7 @@

{% trans "Stock Item Attachments" %}

- -
-
- -
-
- - - - - - - - - - - {% for attachment in item.attachments.all %} - - - - - - {% endfor %} - -
{% trans "File" %}{% trans "Comment" %}
{{ attachment.basename }}{{ attachment.comment }} -
- - -
-
+{% include "attachment_table.html" with attachments=item.attachments.all %} {% endblock %} @@ -60,7 +27,9 @@ $("#new-attachment").click(function() { $("#attachment-table").on('click', '.attachment-edit-button', function() { var button = $(this); - launchModalForm(button.attr('url'), + var url = `/stock/item/attachment/${button.attr('pk')}/edit/`; + + launchModalForm(url, { reload: true, }); @@ -69,7 +38,9 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() { var button = $(this); - launchModalForm(button.attr('url'), { + var url = `/stock/item/attachment/${button.attr('pk')}/delete/`; + + launchModalForm(url, { success: function() { location.reload(); } diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 19edf8666d..e616be1f35 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -160,6 +160,12 @@ class StockItemAttachmentCreate(AjaxCreateView): ajax_form_title = _("Add Stock Item Attachment") ajax_template_name = "modal_form.html" + def post_save(self, **kwargs): + """ Record the user that uploaded the attachment """ + + self.object.user = self.request.user + self.object.save() + def get_data(self): return { 'success': _("Added attachment") diff --git a/InvenTree/templates/attachment_table.html b/InvenTree/templates/attachment_table.html new file mode 100644 index 0000000000..090ae566f6 --- /dev/null +++ b/InvenTree/templates/attachment_table.html @@ -0,0 +1,40 @@ +{% load i18n %} + +
+
+ +
+
+ + + + + + + + + + + + {% for attachment in attachments %} + + + + + + + {% endfor %} + +
{% trans "File" %}{% trans "Comment" %}{% trans "Uploaded" %}
{{ attachment.basename }}{{ attachment.comment }} + {% if attachment.upload_date %}{{ attachment.upload_date }}{% endif %} + {% if attachment.user %}{{ attachment.user.username }}{% endif %} + +
+ + +
+
\ No newline at end of file diff --git a/README.md b/README.md index 7afe6269ae..d4a3dfc869 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,15 @@ Refer to the [getting started guide](https://inventree.github.io/docs/start/inst For InvenTree documentation, refer to the [InvenTre documentation website](https://inventree.github.io). +## Integration + +InvenTree is designed to be extensible, and provides multiple options for integration with external applications or addition of custom plugins: + +* [InvenTree API](https://inventree.github.io/docs/extend/api) +* [Python module](https://inventree.github.io/docs/extend/python) +* [Plugin interface](https://inventree.github.io/docs/extend/plugins) +* [Third party](https://inventree.github.io/docs/extend/integrate) + ## Developer Documentation For code documentation, refer to the [developer documentation](http://inventree.readthedocs.io/en/latest/).