diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index aea55fbbd9..5e43d8d5de 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -60,29 +60,6 @@ class InvenTreeMoneySerializer(MoneyField): return amount -class UserSerializer(serializers.ModelSerializer): - """Serializer for User - provides all fields.""" - - class Meta: - """Metaclass options.""" - - model = User - fields = 'all' - - -class UserSerializerBrief(serializers.ModelSerializer): - """Serializer for User - provides limited information.""" - - class Meta: - """Metaclass options.""" - - model = User - fields = [ - 'pk', - 'username', - ] - - class InvenTreeModelSerializer(serializers.ModelSerializer): """Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation.""" @@ -218,6 +195,21 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): return data +class UserSerializer(InvenTreeModelSerializer): + """Serializer for a User.""" + + class Meta: + """Metaclass defines serializer fields.""" + model = User + fields = [ + 'pk', + 'username', + 'first_name', + 'last_name', + 'email' + ] + + class ReferenceIndexingSerializerMixin(): """This serializer mixin ensures the the reference is not to big / small for the BigIntegerField.""" @@ -239,9 +231,7 @@ class InvenTreeAttachmentSerializerField(serializers.FileField): /media/foo/bar.jpg - Why? You can't handle the why! - - Actually, if the server process is serving the data at 127.0.0.1, + If the server process is serving the data at 127.0.0.1, but a proxy service (e.g. nginx) is then providing DNS lookup to the outside world, then an attachment which prefixes the "address" of the internal server will not be accessible from the outside world. @@ -261,6 +251,8 @@ class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): The only real addition here is that we support "renaming" of the attachment file. """ + user_detail = UserSerializer(source='user', read_only=True, many=False) + attachment = InvenTreeAttachmentSerializerField( required=False, allow_null=False, diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 5d095d9e8f..cd6c6ba82a 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -413,7 +413,7 @@ class BuildItemList(generics.ListCreateAPIView): ] -class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin): +class BuildAttachmentList(AttachmentMixin, generics.ListCreateAPIView): """API endpoint for listing (and creating) BuildOrderAttachment objects.""" queryset = BuildOrderAttachment.objects.all() @@ -428,7 +428,7 @@ class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin): ] -class BuildAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): +class BuildAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): """Detail endpoint for a BuildOrderAttachment object.""" queryset = BuildOrderAttachment.objects.all() diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 6659dd1e42..daf142cf5d 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -11,7 +11,7 @@ from rest_framework import serializers from rest_framework.serializers import ValidationError from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer -from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin +from InvenTree.serializers import ReferenceIndexingSerializerMixin, UserSerializer import InvenTree.helpers from InvenTree.helpers import extract_serial_numbers @@ -40,7 +40,7 @@ class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer overdue = serializers.BooleanField(required=False, read_only=True) - issued_by_detail = UserSerializerBrief(source='issued_by', read_only=True) + issued_by_detail = UserSerializer(source='issued_by', read_only=True) responsible_detail = OwnerSerializer(source='responsible', read_only=True) @@ -860,6 +860,8 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer): 'filename', 'comment', 'upload_date', + 'user', + 'user_detail', ] read_only_fields = [ diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index bf991fe370..b48e5ff1da 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -965,12 +965,19 @@ class InvenTreeSetting(BaseInvenTreeSetting): }, 'REPORT_ENABLE_TEST_REPORT': { - 'name': _('Test Reports'), + 'name': _('Enable Test Reports'), 'description': _('Enable generation of test reports'), 'default': True, 'validator': bool, }, + 'REPORT_ATTACH_TEST_REPORT': { + 'name': _('Attach Test Reports'), + 'description': _('When printing a Test Report, attach a copy of the Test Report to the associated Stock Item'), + 'default': False, + 'validator': bool, + }, + 'STOCK_BATCH_CODE_TEMPLATE': { 'name': _('Batch Code Template'), 'description': _('Template for generating default batch codes for stock items'), diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index e1d95637b8..a24945e949 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -158,6 +158,8 @@ class ManufacturerPartAttachmentSerializer(InvenTreeAttachmentSerializer): 'link', 'comment', 'upload_date', + 'user', + 'user_detail', ] read_only_fields = [ diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 4303979d42..9ad123577d 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -527,7 +527,7 @@ class PurchaseOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = serializers.PurchaseOrderExtraLineSerializer -class SalesOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin): +class SalesOrderAttachmentList(AttachmentMixin, generics.ListCreateAPIView): """API endpoint for listing (and creating) a SalesOrderAttachment (file upload)""" queryset = models.SalesOrderAttachment.objects.all() @@ -542,7 +542,7 @@ class SalesOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin): ] -class SalesOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): +class SalesOrderAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): """Detail endpoint for SalesOrderAttachment.""" queryset = models.SalesOrderAttachment.objects.all() @@ -1056,7 +1056,7 @@ class SalesOrderShipmentComplete(generics.CreateAPIView): return ctx -class PurchaseOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin): +class PurchaseOrderAttachmentList(AttachmentMixin, generics.ListCreateAPIView): """API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)""" queryset = models.PurchaseOrderAttachment.objects.all() @@ -1071,7 +1071,7 @@ class PurchaseOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin): ] -class PurchaseOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): +class PurchaseOrderAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): """Detail endpoint for a PurchaseOrderAttachment.""" queryset = models.PurchaseOrderAttachment.objects.all() diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index e028c8a31a..757559da06 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -630,6 +630,8 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer): 'filename', 'comment', 'upload_date', + 'user', + 'user_detail', ] read_only_fields = [ @@ -1348,6 +1350,8 @@ class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer): 'link', 'comment', 'upload_date', + 'user', + 'user_detail', ] read_only_fields = [ diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 9e50cabe3a..31ae1adca5 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -302,7 +302,7 @@ class PartInternalPriceList(generics.ListCreateAPIView): ] -class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin): +class PartAttachmentList(AttachmentMixin, generics.ListCreateAPIView): """API endpoint for listing (and creating) a PartAttachment (file upload).""" queryset = PartAttachment.objects.all() @@ -317,7 +317,7 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin): ] -class PartAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): +class PartAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): """Detail endpoint for PartAttachment model.""" queryset = PartAttachment.objects.all() diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index c2e674db89..11d3280112 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -94,6 +94,8 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer): 'link', 'comment', 'upload_date', + 'user', + 'user_detail', ] read_only_fields = [ diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index 96a6cd34e5..d8721eb7d7 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -1,6 +1,7 @@ """API functionality for the 'report' app""" from django.core.exceptions import FieldError, ValidationError +from django.core.files.base import ContentFile from django.http import HttpResponse from django.template.exceptions import TemplateDoesNotExist from django.urls import include, path, re_path @@ -15,7 +16,7 @@ import common.models import InvenTree.helpers import order.models import part.models -from stock.models import StockItem +from stock.models import StockItem, StockItemAttachment from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport, SalesOrderReport, TestReport) @@ -158,6 +159,18 @@ class PartReportMixin: class ReportPrintMixin: """Mixin for printing reports.""" + def report_callback(self, object, report, request): + """Callback function for each object/report combination. + + Allows functionality to be performed before returning the consolidated PDF + + Arguments: + object: The model instance to be printed + report: The individual PDF file object + request: The request instance associated with this print call + """ + ... + def print(self, request, items_to_print): """Print this report template against a number of pre-validated items.""" if len(items_to_print) == 0: @@ -182,12 +195,16 @@ class ReportPrintMixin: report.object_to_print = item report_name = report.generate_filename(request) + output = report.render(request) + + # Run report callback for each generated report + self.report_callback(item, output, request) try: if debug_mode: outputs.append(report.render_as_string(request)) else: - outputs.append(report.render(request)) + outputs.append(output) except TemplateDoesNotExist as e: template = str(e) if not template: @@ -326,6 +343,22 @@ class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin, R queryset = TestReport.objects.all() serializer_class = TestReportSerializer + def report_callback(self, item, report, request): + """Callback to (optionally) save a copy of the generated report""" + + if common.models.InvenTreeSetting.get_setting('REPORT_ATTACH_TEST_REPORT'): + + # Construct a PDF file object + pdf = report.get_document().write_pdf() + pdf_content = ContentFile(pdf, "test_report.pdf") + + StockItemAttachment.objects.create( + attachment=pdf_content, + stock_item=item, + user=request.user, + comment=_("Test report") + ) + def get(self, request, *args, **kwargs): """Check if valid stock item(s) have been provided.""" items = self.get_items() diff --git a/InvenTree/report/tests.py b/InvenTree/report/tests.py index e6a89a0304..336f48d6fc 100644 --- a/InvenTree/report/tests.py +++ b/InvenTree/report/tests.py @@ -9,9 +9,9 @@ from django.urls import reverse import report.models as report_models from build.models import Build -from common.models import InvenTreeUserSetting +from common.models import InvenTreeSetting, InvenTreeUserSetting from InvenTree.api_tester import InvenTreeAPITestCase -from stock.models import StockItem +from stock.models import StockItem, StockItemAttachment class ReportTest(InvenTreeAPITestCase): @@ -141,15 +141,28 @@ class TestReportTest(ReportTest): # Now print with a valid StockItem item = StockItem.objects.first() - response = self.get(url, {'item': item.pk}) + response = self.get(url, {'item': item.pk}, expected_code=200) # Response should be a StreamingHttpResponse (PDF file) self.assertEqual(type(response), StreamingHttpResponse) headers = response.headers - self.assertEqual(headers['Content-Type'], 'application/pdf') + # By default, this should *not* have created an attachment against this stockitem + self.assertFalse(StockItemAttachment.objects.filter(stock_item=item).exists()) + + # Change the setting, now the test report should be attached automatically + InvenTreeSetting.set_setting('REPORT_ATTACH_TEST_REPORT', True, None) + + response = self.get(url, {'item': item.pk}, expected_code=200) + headers = response.headers + self.assertEqual(headers['Content-Type'], 'application/pdf') + + # Check that a report has been uploaded + attachment = StockItemAttachment.objects.filter(stock_item=item).first() + self.assertIsNotNone(attachment) + class BuildReportTest(ReportTest): """Unit test class for the BuildReport model""" diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 53205dd7b9..d22c0c37dc 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -1043,7 +1043,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView): ] -class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin): +class StockAttachmentList(AttachmentMixin, generics.ListCreateAPIView): """API endpoint for listing (and creating) a StockItemAttachment (file upload).""" queryset = StockItemAttachment.objects.all() @@ -1060,7 +1060,7 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin): ] -class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): +class StockAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): """Detail endpoint for StockItemAttachment.""" queryset = StockItemAttachment.objects.all() diff --git a/InvenTree/stock/migrations/0071_auto_20211205_1733.py b/InvenTree/stock/migrations/0071_auto_20211205_1733.py index a6379d899d..0e8ecba1b5 100644 --- a/InvenTree/stock/migrations/0071_auto_20211205_1733.py +++ b/InvenTree/stock/migrations/0071_auto_20211205_1733.py @@ -20,9 +20,9 @@ def delete_scheduled(apps, schema_editor): items = StockItem.objects.filter(scheduled_for_deletion=True) - logger.info(f"Removing {items.count()} stock items scheduled for deletion") - - items.delete() + if items.count() > 0: + logger.info(f"Removing {items.count()} stock items scheduled for deletion") + items.delete() Task = apps.get_model('django_q', 'schedule') diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 96dfb0b839..aebc102e79 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -549,19 +549,6 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer): class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer): """Serializer for StockItemAttachment model.""" - def __init__(self, *args, **kwargs): - """Add detail fields.""" - user_detail = kwargs.pop('user_detail', False) - - super().__init__(*args, **kwargs) - - if user_detail is not True: - self.fields.pop('user_detail') - - user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True) - - # TODO: Record the uploading user when creating or updating an attachment! - class Meta: """Metaclass options.""" @@ -589,7 +576,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for the StockItemTestResult model.""" - user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True) + user_detail = InvenTree.serializers.UserSerializer(source='user', read_only=True) key = serializers.CharField(read_only=True) @@ -650,7 +637,7 @@ class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer): item_detail = StockItemSerializerBrief(source='item', many=False, read_only=True) - user_detail = InvenTree.serializers.UserSerializerBrief(source='user', many=False, read_only=True) + user_detail = InvenTree.serializers.UserSerializer(source='user', many=False, read_only=True) deltas = serializers.JSONField(read_only=True) diff --git a/InvenTree/templates/InvenTree/settings/report.html b/InvenTree/templates/InvenTree/settings/report.html index 3c6528e1e8..030d5fa48d 100644 --- a/InvenTree/templates/InvenTree/settings/report.html +++ b/InvenTree/templates/InvenTree/settings/report.html @@ -16,6 +16,7 @@ {% include "InvenTree/settings/setting.html" with key="REPORT_DEFAULT_PAGE_SIZE" icon="fa-print" %} {% include "InvenTree/settings/setting.html" with key="REPORT_DEBUG_MODE" icon="fa-laptop-code" %} {% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" icon="fa-vial" %} + {% include "InvenTree/settings/setting.html" with key="REPORT_ATTACH_TEST_REPORT" icon="fa-file-upload" %} diff --git a/InvenTree/templates/js/translated/attachment.js b/InvenTree/templates/js/translated/attachment.js index 6367037005..7d89725a65 100644 --- a/InvenTree/templates/js/translated/attachment.js +++ b/InvenTree/templates/js/translated/attachment.js @@ -244,8 +244,14 @@ function loadAttachmentTable(url, options) { { field: 'upload_date', title: '{% trans "Upload Date" %}', - formatter: function(value) { - return renderDate(value); + formatter: function(value, row) { + var html = renderDate(value); + + if (row.user_detail) { + html += `${row.user_detail.username}`; + } + + return html; } }, { diff --git a/InvenTree/users/api.py b/InvenTree/users/api.py index 332c5ec67d..11267e4d8f 100644 --- a/InvenTree/users/api.py +++ b/InvenTree/users/api.py @@ -10,8 +10,9 @@ from rest_framework.authtoken.models import Token from rest_framework.response import Response from rest_framework.views import APIView +from InvenTree.serializers import UserSerializer from users.models import Owner, RuleSet, check_user_role -from users.serializers import OwnerSerializer, UserSerializer +from users.serializers import OwnerSerializer class OwnerList(generics.ListAPIView): diff --git a/InvenTree/users/serializers.py b/InvenTree/users/serializers.py index 1ae2dc28e1..bde5cb87db 100644 --- a/InvenTree/users/serializers.py +++ b/InvenTree/users/serializers.py @@ -1,6 +1,5 @@ """DRF API serializers for the 'users' app""" -from django.contrib.auth.models import User from rest_framework import serializers @@ -9,19 +8,6 @@ from InvenTree.serializers import InvenTreeModelSerializer from .models import Owner -class UserSerializer(InvenTreeModelSerializer): - """Serializer for a User.""" - - class Meta: - """Metaclass defines serializer fields.""" - model = User - fields = ('pk', - 'username', - 'first_name', - 'last_name', - 'email',) - - class OwnerSerializer(InvenTreeModelSerializer): """Serializer for an "Owner" (either a "user" or a "group")"""