From cbb286e46db05a169ca399010b44a7d7227bd4d6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 18 Jan 2021 20:55:30 +1100 Subject: [PATCH 01/10] Add API for stock item test report --- InvenTree/InvenTree/urls.py | 2 + InvenTree/report/api.py | 158 ++++++++++++++++++++++++++++++++ InvenTree/report/serializers.py | 23 +++++ 3 files changed, 183 insertions(+) create mode 100644 InvenTree/report/api.py create mode 100644 InvenTree/report/serializers.py diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d8a64708a9..503e373641 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -29,6 +29,7 @@ from stock.api import stock_api_urls from build.api import build_api_urls from order.api import order_api_urls from label.api import label_api_urls +from report.api import report_api_urls from django.conf import settings from django.conf.urls.static import static @@ -60,6 +61,7 @@ apipatterns = [ url(r'^build/', include(build_api_urls)), url(r'^order/', include(order_api_urls)), url(r'^label/', include(label_api_urls)), + url(r'^report/', include(report_api_urls)), # User URLs url(r'^user/', include(user_urls)), diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py new file mode 100644 index 0000000000..ed70da752b --- /dev/null +++ b/InvenTree/report/api.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import sys + +from django.utils.translation import ugettext as _ +from django.conf.urls import url, include + +from django_filters.rest_framework import DjangoFilterBackend + +from rest_framework import generics, filters +from rest_framework.response import Response + +import InvenTree.helpers + +from stock.models import StockItem + +from .models import TestReport +from .serializers import TestReportSerializer + + +class ReportListView(generics.ListAPIView): + """ + Generic API class for report templates + """ + + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + ] + + filter_fields = [ + 'enabled', + ] + + search_fields = [ + 'name', + 'description', + ] + + +class StockItemReportMixin: + """ + Mixin for extracting stock items from query params + """ + + def get_items(self): + """ + Return a list of requested stock items + """ + + items = [] + + params = self.request.query_params + + if 'items[]' in params: + items = params.getlist('items[]', []) + elif 'item' in params: + items = [params.get('item', None)] + + if type(items) not in [list, tuple]: + item = [items] + + valid_ids = [] + + for item in items: + try: + valid_ids.append(int(item)) + except (ValueError): + pass + + # List of StockItems which match provided values + valid_items = StockItem.objects.filter(pk__in=valid_ids) + + return valid_items + + +class StockItemTestReportList(ReportListView, StockItemReportMixin): + """ + API endpoint for viewing list of TestReport objects. + + Filterable by: + + - enabled: Filter by enabled / disabled status + - item: Filter by single stock item + - items: Filter by list of stock items + + """ + + queryset = TestReport.objects.all() + serializer_class = TestReportSerializer + + def filter_queryset(self, queryset): + + queryset = super().filter_queryset(queryset) + + # List of StockItem objects to match against + items = self.get_items() + + if len(items) > 0: + """ + We wish to filter by stock items. + + We need to compare the 'filters' string of each report, + and see if it matches against each of the specified stock items. + + TODO: In the future, perhaps there is a way to make this more efficient. + """ + + valid_report_ids = set() + + for report in queryset.all(): + + matches = True + + # Filter string defined for the report object + filters = InvenTree.helpers.validateFilterString(report.filters) + + for item in items: + item_query = StockItem.objects.filter(pk=item.pk) + + if not item_query.filter(**filters).exists(): + matches = False + break + + if matches: + valid_report_ids.add(report.pk) + else: + continue + + # Reduce queryset to only valid matches + queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids]) + return queryset + + +class StockItemTestReportDetail(generics.RetrieveUpdateDestroyAPIView): + """ + API endpoint for a single TestReport object + """ + + queryset = TestReport.objects.all() + serializer_class = TestReportSerializer + + +report_api_urls = [ + + # Stock item test reports + url(r'test/', include([ + # Detail views + url(r'^(?P\d+)/', include([ + #url(r'print/?', StockItemTestReportPrint.as_view(), name='api-stockitem-testreport-print'), + url(r'^.*$', StockItemTestReportDetail.as_view(), name='api-stockitem-testreport-detail'), + ])), + + # List view + url(r'^.*$', StockItemTestReportList.as_view(), name='api-stockitem-testreport-list'), + ])), +] \ No newline at end of file diff --git a/InvenTree/report/serializers.py b/InvenTree/report/serializers.py new file mode 100644 index 0000000000..0cd4d4f40a --- /dev/null +++ b/InvenTree/report/serializers.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from InvenTree.serializers import InvenTreeModelSerializer +from InvenTree.serializers import InvenTreeAttachmentSerializerField + +from .models import TestReport + + +class TestReportSerializer(InvenTreeModelSerializer): + + template = InvenTreeAttachmentSerializerField(required=True) + + class Meta: + model = TestReport + fields = [ + 'pk', + 'name', + 'description', + 'template', + 'filters', + 'enabled', + ] From 1b835a71dfbe6e8f348a33c727b352fd20397f4b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 18 Jan 2021 21:17:19 +1100 Subject: [PATCH 02/10] Print one (or more!) report templates via API --- InvenTree/label/api.py | 2 +- InvenTree/report/api.py | 58 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 48ead2f443..6b542f80ed 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -310,7 +310,7 @@ class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin) """ queryset = StockLocationLabel.objects.all() - seiralizers_class = StockLocationLabelSerializer + seiralizer_class = StockLocationLabelSerializer def get(self, request, *args, **kwargs): diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index ed70da752b..aaffc47a9c 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import sys - from django.utils.translation import ugettext as _ from django.conf.urls import url, include @@ -142,17 +140,69 @@ class StockItemTestReportDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = TestReportSerializer +class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin): + """ + API endpoint for printing a TestReport object + """ + + queryset = TestReport.objects.all() + serializer_class = TestReportSerializer + + def get(self, request, *args, **kwargs): + """ + Check if valid stock item(s) have been provided. + """ + + items = self.get_items() + + if len(items) == 0: + # No valid items provided, return an error message + data = { + 'error': _('Must provide valid StockItem(s)') + } + + return Response(data, status=400) + + outputs = [] + + # Merge one or more PDF files into a single download + for item in items: + report = self.get_object() + report.stock_item = item + + outputs.append(report.render(request)) + + pages = [] + + if len(outputs) > 1: + # If more than one output is generated, merge them into a single file + for output in outputs: + doc = output.get_document() + for page in doc.pages: + pages.append(page) + + pdf = outputs[0].get_document().copy(pages).write_pdf() + else: + pdf = outputs[0].get_document().write_pdf() + + return InvenTree.helpers.DownloadFile( + pdf, + 'test_report.pdf', + content_type='application/pdf' + ) + + report_api_urls = [ # Stock item test reports url(r'test/', include([ # Detail views url(r'^(?P\d+)/', include([ - #url(r'print/?', StockItemTestReportPrint.as_view(), name='api-stockitem-testreport-print'), + url(r'print/?', StockItemTestReportPrint.as_view(), name='api-stockitem-testreport-print'), url(r'^.*$', StockItemTestReportDetail.as_view(), name='api-stockitem-testreport-detail'), ])), # List view url(r'^.*$', StockItemTestReportList.as_view(), name='api-stockitem-testreport-list'), ])), -] \ No newline at end of file +] From ef7cc3f78dce2f942db24f4eff16cc9d1f897770 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 18 Jan 2021 21:33:15 +1100 Subject: [PATCH 03/10] Replace existing django form views with API request --- InvenTree/InvenTree/urls.py | 1 + .../stock/templates/stock/item_base.html | 7 +- InvenTree/stock/urls.py | 4 - InvenTree/stock/views.py | 87 ------------ InvenTree/templates/base.html | 1 + InvenTree/templates/js/label.js | 2 +- InvenTree/templates/js/report.js | 124 ++++++++++++++++++ 7 files changed, 128 insertions(+), 98 deletions(-) create mode 100644 InvenTree/templates/js/report.js diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 503e373641..9ad7780122 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -103,6 +103,7 @@ dynamic_javascript_urls = [ url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'), url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'), url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'), + url(r'^report.js', DynamicJsView.as_view(template_name='js/report.js'), name='report.js'), url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'), url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'), ] diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 8e4693cbc1..403f35d5bb 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -394,12 +394,7 @@ $('#stock-uninstall').click(function() { }); $("#stock-test-report").click(function() { - launchModalForm( - "{% url 'stock-item-test-report-select' item.id %}", - { - follow: true, - } - ); + printTestReports([{{ item.pk }}]); }); $("#print-label").click(function() { diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 6aeb1f5b73..93758588ce 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -29,8 +29,6 @@ stock_item_detail_urls = [ url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'), - url(r'^test-report-select/', views.StockItemTestReportSelect.as_view(), name='stock-item-test-report-select'), - url(r'^test/', views.StockItemDetail.as_view(template_name='stock/item_tests.html'), name='stock-item-test-results'), 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'), @@ -62,8 +60,6 @@ stock_urls = [ url(r'^item/uninstall/', views.StockItemUninstall.as_view(), name='stock-item-uninstall'), - url(r'^item/test-report-download/', views.StockItemTestReportDownload.as_view(), name='stock-item-test-report-download'), - # URLs for StockItem attachments url(r'^item/attachment/', include([ url(r'^new/', views.StockItemAttachmentCreate.as_view(), name='stock-item-attachment-create'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 123fa4d8fb..2de775422a 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -30,7 +30,6 @@ from datetime import datetime, timedelta from company.models import Company, SupplierPart from part.models import Part -from report.models import TestReport from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult import common.settings @@ -410,92 +409,6 @@ class StockItemTestResultDelete(AjaxDeleteView): role_required = 'stock.delete' -class StockItemTestReportSelect(AjaxView): - """ - View for selecting a TestReport template, - and generating a TestReport as a PDF. - """ - - model = StockItem - ajax_form_title = _("Select Test Report Template") - role_required = 'stock.view' - - def get_form(self): - - stock_item = StockItem.objects.get(pk=self.kwargs['pk']) - form = StockForms.TestReportFormatForm(stock_item) - - return form - - def get_initial(self): - - initials = super().get_initial() - - form = self.get_form() - options = form.fields['template'].queryset - - # If only a single template is available, pre-select it - if options.count() == 1: - initials['template'] = options[0] - - return initials - - def post(self, request, *args, **kwargs): - - template_id = request.POST.get('template', None) - - try: - template = TestReport.objects.get(pk=template_id) - except (ValueError, TestReport.DoesNoteExist): - raise ValidationError({'template': _("Select valid template")}) - - stock_item = StockItem.objects.get(pk=self.kwargs['pk']) - - url = reverse('stock-item-test-report-download') - - url += '?stock_item={id}'.format(id=stock_item.pk) - url += '&template={id}'.format(id=template.pk) - - data = { - 'form_valid': True, - 'url': url, - } - - return self.renderJsonResponse(request, self.get_form(), data=data) - - -class StockItemTestReportDownload(AjaxView): - """ - Download a TestReport against a StockItem. - - Requires the following arguments to be passed as URL params: - - stock_item - Valid PK of a StockItem object - template - Valid PK of a TestReport template object - - """ - role_required = 'stock.view' - - def get(self, request, *args, **kwargs): - - template = request.GET.get('template', None) - stock_item = request.GET.get('stock_item', None) - - try: - template = TestReport.objects.get(pk=template) - except (ValueError, TestReport.DoesNotExist): - raise ValidationError({'template': 'Invalid template ID'}) - - try: - stock_item = StockItem.objects.get(pk=stock_item) - except (ValueError, StockItem.DoesNotExist): - raise ValidationError({'stock_item': 'Invalid StockItem ID'}) - - template.stock_item = stock_item - - return template.render(request) - - class StockExportOptions(AjaxView): """ Form for selecting StockExport options """ diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 36f4974816..f7ee1b2f19 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -121,6 +121,7 @@ InvenTree + diff --git a/InvenTree/templates/js/label.js b/InvenTree/templates/js/label.js index 93eb1c60c5..b62cbc9ef0 100644 --- a/InvenTree/templates/js/label.js +++ b/InvenTree/templates/js/label.js @@ -170,4 +170,4 @@ function selectLabel(labels, items, options={}) { options.success(pk); } }); -} \ No newline at end of file +} diff --git a/InvenTree/templates/js/report.js b/InvenTree/templates/js/report.js new file mode 100644 index 0000000000..3216e5dfe1 --- /dev/null +++ b/InvenTree/templates/js/report.js @@ -0,0 +1,124 @@ +{% load i18n %} + + +function selectTestReport(reports, items, options={}) { + /** + * Present the user with the available test reports, + * and allow them to select which test report to print. + * + * The intent is that the available report templates have been requested + * (via AJAX) from the server. + */ + + var modal = options.modal || '#modal-form'; + + var report_list = makeOptionsList( + reports, + function(item) { + var text = item.name; + + if (item.description) { + text += ` - ${item.description}`; + } + + return text; + }, + function(item) { + return item.pk; + } + ); + + // Construct form + var html = ` + +
+
+ +
+ +
+
+
`; + + openModal({ + modal: modal, + }); + + modalEnable(modal, true); + modalSetTitle(modal, '{% trans "Select Test Report Template" %}'); + modalSetContent(modal, html); + + attachSelect(modal); + + modalSubmit(modal, function() { + + var label = $(modal).find('#id_report'); + + var pk = label.val(); + + closeModal(modal); + + if (options.success) { + options.success(pk); + } + }); + +} + + +function printTestReports(items, options={}) { + /** + * Print test reports for the provided stock item(s) + */ + + if (items.length == 0) { + showAlertDialog( + '{% trans "Select Stock Items" %}', + '{% trans "Stock item(s) must be selected before printing reports" %}' + ); + + return; + } + + // Request available labels from the server + inventreeGet( + '{% url "api-stockitem-testreport-list" %}', + { + enabled: true, + items: items, + }, + { + success: function(response) { + if (response.length == 0) { + showAlertDialog( + '{% trans "No Reports Found" %}', + '{% trans "No report templates found which match selected stock item(s)" %}', + ); + + return; + } + + // Select report template to print + selectTestReport( + response, + items, + { + success: function(pk) { + var href = `/api/report/test/${pk}/print/?`; + + items.forEach(function(item) { + href += `items[]=${item}&`; + }); + + window.location.href = href; + } + } + ); + } + } + ); +} \ No newline at end of file From 0a566c062dd5d0014772fb2e5af4edc4bd186756 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 18 Jan 2021 21:36:37 +1100 Subject: [PATCH 04/10] Add click callback on item test tab --- InvenTree/stock/templates/stock/item_tests.html | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/InvenTree/stock/templates/stock/item_tests.html b/InvenTree/stock/templates/stock/item_tests.html index 11ab443d0c..e2ccf22037 100644 --- a/InvenTree/stock/templates/stock/item_tests.html +++ b/InvenTree/stock/templates/stock/item_tests.html @@ -50,14 +50,9 @@ function reloadTable() { //$("#test-result-table").bootstrapTable("refresh"); } -{% if item.part.has_test_report_templates %} +{% if item.has_test_reports %} $("#test-report").click(function() { - launchModalForm( - "{% url 'stock-item-test-report-select' item.id %}", - { - follow: true, - } - ); + printTestReports([{{ item.pk }}]); }); {% endif %} From 952da196000f92164a85b99255033b7d0498be05 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 18 Jan 2021 21:42:55 +1100 Subject: [PATCH 05/10] Print test report for multiple stock items at once --- InvenTree/templates/js/report.js | 11 ++++++++++- InvenTree/templates/js/stock.js | 13 +++++++++++++ InvenTree/templates/stock_table.html | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/report.js b/InvenTree/templates/js/report.js index 3216e5dfe1..da84433bac 100644 --- a/InvenTree/templates/js/report.js +++ b/InvenTree/templates/js/report.js @@ -29,8 +29,17 @@ function selectTestReport(reports, items, options={}) { ); // Construct form - var html = ` + var html = ''; + if (items.length > 0) { + + html += ` +
+ ${items.length} {% trans "stock items selected" %} +
`; + } + + html += `