From bdc7367e293ea9570a4d8131a9b4ac908099a201 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 9 Jan 2021 20:43:48 +1100 Subject: [PATCH] Add endpoint for printing labels --- InvenTree/InvenTree/urls.py | 2 + InvenTree/label/api.py | 84 +++++++++++++++---- InvenTree/templates/base.html | 3 +- InvenTree/templates/js/label.js | 52 ++++++++++++ .../inventree => templates/js}/modals.js | 50 +++++------ InvenTree/templates/js/stock.js | 33 ++++++++ InvenTree/templates/stock_table.html | 1 + 7 files changed, 184 insertions(+), 41 deletions(-) create mode 100644 InvenTree/templates/js/label.js rename InvenTree/{InvenTree/static/script/inventree => templates/js}/modals.js (92%) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 019f939f60..d8a64708a9 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -92,6 +92,7 @@ settings_urls = [ # Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer dynamic_javascript_urls = [ + url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'), url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'), url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'), url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'), @@ -99,6 +100,7 @@ dynamic_javascript_urls = [ url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'), 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'^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/label/api.py b/InvenTree/label/api.py index 8f3828410e..e3367ad894 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -1,11 +1,15 @@ # -*- 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 @@ -35,21 +39,11 @@ class LabelListView(generics.ListAPIView): ] -class StockItemLabelList(LabelListView): +class StockItemLabelMixin: """ - API endpoint for viewing list of StockItemLabel objects. - - Filterable by: - - - enabled: Filter by enabled / disabled status - - item: Filter by single stock item - - items: Filter by list of stock items - + Mixin for extracting stock items from query params """ - queryset = StockItemLabel.objects.all() - serializer_class = StockItemLabelSerializer - def get_items(self): """ Return a list of requested stock items @@ -59,8 +53,8 @@ class StockItemLabelList(LabelListView): params = self.request.query_params - if 'items' in params: - items = params.getlist('items', []) + if 'items[]' in params: + items = params.getlist('items[]', []) elif 'item' in params: items = [params.get('item', None)] @@ -80,6 +74,22 @@ class StockItemLabelList(LabelListView): return valid_items + +class StockItemLabelList(LabelListView, StockItemLabelMixin): + """ + API endpoint for viewing list of StockItemLabel objects. + + Filterable by: + + - enabled: Filter by enabled / disabled status + - item: Filter by single stock item + - items: Filter by list of stock items + + """ + + queryset = StockItemLabel.objects.all() + serializer_class = StockItemLabelSerializer + def filter_queryset(self, queryset): """ Filter the StockItem label queryset. @@ -140,6 +150,47 @@ class StockItemLabelDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = StockItemLabelSerializer +class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin): + """ + API endpoint for printing a StockItemLabel object + """ + + queryset = StockItemLabel.objects.all() + serializer_class = StockItemLabelSerializer + + 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) + + label = self.get_object() + + try: + pdf = label.render(items) + except: + + e = sys.exc_info()[1] + + data = { + 'error': _('Error during label printing'), + 'message': str(e), + } + + return Response(data, status=400) + + return InvenTree.helpers.DownloadFile(pdf.getbuffer(), 'stock_item_labels.pdf', content_type='application/pdf') + + class StockLocationLabelList(LabelListView): """ API endpoint for viewiing list of StockLocationLabel objects. @@ -163,7 +214,7 @@ class StockLocationLabelList(LabelListView): params = self.request.query_params - if 'locations' in params: + if 'locations[]' in params: locations = params.getlist('locations', []) elif 'location' in params: locations = [params.get('location', None)] @@ -175,7 +226,7 @@ class StockLocationLabelList(LabelListView): for loc in locations: try: - valid_ids.append(int(item)) + valid_ids.append(int(loc)) except (ValueError): pass @@ -249,6 +300,7 @@ label_api_urls = [ url(r'stock/', include([ # Detail views url(r'^(?P\d+)/', include([ + url(r'print/?', StockItemLabelPrint.as_view(), name='api-stockitem-label-print'), url(r'^.*$', StockItemLabelDetail.as_view(), name='api-stockitem-label-detail'), ])), diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index c025919936..36f4974816 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -112,7 +112,6 @@ InvenTree - @@ -120,6 +119,8 @@ InvenTree + + diff --git a/InvenTree/templates/js/label.js b/InvenTree/templates/js/label.js new file mode 100644 index 0000000000..28a377df8b --- /dev/null +++ b/InvenTree/templates/js/label.js @@ -0,0 +1,52 @@ +{% load i18n %} + +function selectLabel(labels, options={}) { + /** + * Present the user with the available labels, + * and allow them to select which label to print. + * + * The intent is that the available labels have been requested + * (via AJAX) from the server. + */ + + var modal = options.modal || '#modal-form'; + + var label_list = makeOptionsList( + labels, + 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 Label Template" %}'); + modalSetContent(modal, html); +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/script/inventree/modals.js b/InvenTree/templates/js/modals.js similarity index 92% rename from InvenTree/InvenTree/static/script/inventree/modals.js rename to InvenTree/templates/js/modals.js index 12a496c481..08306352b4 100644 --- a/InvenTree/InvenTree/static/script/inventree/modals.js +++ b/InvenTree/templates/js/modals.js @@ -1,3 +1,5 @@ +{% load i18n %} + function makeOption(text, value, title) { /* Format an option for a select element */ @@ -392,7 +394,7 @@ function renderErrorMessage(xhr) {
@@ -459,8 +461,8 @@ function showQuestionDialog(title, content, options={}) { modalSetTitle(modal, title); modalSetContent(modal, content); - var accept_text = options.accept_text || 'Accept'; - var cancel_text = options.cancel_text || 'Cancel'; + var accept_text = options.accept_text || '{% trans "Accept" %}'; + var cancel_text = options.cancel_text || '{% trans "Cancel" %}'; $(modal).find('#modal-form-cancel').html(cancel_text); $(modal).find('#modal-form-accept').html(accept_text); @@ -524,7 +526,7 @@ function openModal(options) { if (options.title) { modalSetTitle(modal, options.title); } else { - modalSetTitle(modal, 'Loading Data...'); + modalSetTitle(modal, '{% trans "Loading Data" %}...'); } // Unless the content is explicitly set, display loading message @@ -535,8 +537,8 @@ function openModal(options) { } // Default labels for 'Submit' and 'Close' buttons in the form - var submit_text = options.submit_text || 'Submit'; - var close_text = options.close_text || 'Close'; + var submit_text = options.submit_text || '{% trans "Submit" %}'; + var close_text = options.close_text || '{% trans "Close" %}'; modalSetButtonText(modal, submit_text, close_text); @@ -745,7 +747,7 @@ function handleModalForm(url, options) { } else { $(modal).modal('hide'); - showAlertDialog('Invalid response from server', 'Form data missing from server response'); + showAlertDialog('{% trans "Invalid response from server" %}', '{% trans "Form data missing from server response" %}'); } } } @@ -758,7 +760,7 @@ function handleModalForm(url, options) { // There was an error submitting form data via POST $(modal).modal('hide'); - showAlertDialog('Error posting form data', renderErrorMessage(xhr)); + showAlertDialog('{% trans "Error posting form data" %}', renderErrorMessage(xhr)); }, complete: function(xhr) { //TODO @@ -793,8 +795,8 @@ function launchModalForm(url, options = {}) { var modal = options.modal || '#modal-form'; // Default labels for 'Submit' and 'Close' buttons in the form - var submit_text = options.submit_text || 'Submit'; - var close_text = options.close_text || 'Close'; + var submit_text = options.submit_text || '{% trans "Submit" %}'; + var close_text = options.close_text || '{% trans "Close" %}'; // Form the ajax request to retrieve the django form data ajax_data = { @@ -842,7 +844,7 @@ function launchModalForm(url, options = {}) { } else { $(modal).modal('hide'); - showAlertDialog('Invalid server response', 'JSON response missing form data'); + showAlertDialog('{% trans "Invalid server response" %}', '{% trans "JSON response missing form data" %}'); } }, error: function (xhr, ajaxOptions, thrownError) { @@ -852,36 +854,36 @@ function launchModalForm(url, options = {}) { if (xhr.status == 0) { // No response from the server showAlertDialog( - "No Response", - "No response from the InvenTree server", + '{% trans "No Response" %}', + '{% trans "No response from the InvenTree server" %}', ); } else if (xhr.status == 400) { showAlertDialog( - "Error 400: Bad Request", - "Server returned error code 400" + '{% trans "Error 400: Bad Request" %}', + '{% trans "Server returned error code 400" %}', ); } else if (xhr.status == 401) { showAlertDialog( - "Error 401: Not Authenticated", - "Authentication credentials not supplied" + '{% trans "Error 401: Not Authenticated" %}', + '{% trans "Authentication credentials not supplied" %}', ); } else if (xhr.status == 403) { showAlertDialog( - "Error 403: Permission Denied", - "You do not have the required permissions to access this function" + '{% trans "Error 403: Permission Denied" %}', + '{% trans "You do not have the required permissions to access this function" %}', ); } else if (xhr.status == 404) { showAlertDialog( - "Error 404: Resource Not Found", - "The requested resource could not be located on the server" + '{% trans "Error 404: Resource Not Found" %}', + '{% trans "The requested resource could not be located on the server" %}', ); } else if (xhr.status == 408) { showAlertDialog( - "Error 408: Timeout", - "Connection timeout while requesting data from server" + '{% trans "Error 408: Timeout" %}', + '{% trans "Connection timeout while requesting data from server" %}', ); } else { - showAlertDialog('Error requesting form data', renderErrorMessage(xhr)); + showAlertDialog('{% trans "Error requesting form data" %}', renderErrorMessage(xhr)); } console.log("Modal form error: " + xhr.status); diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index e3f124d252..1b448aa957 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -645,6 +645,39 @@ function loadStockTable(table, options) { } // Automatically link button callbacks + $('#multi-item-print-label').click(function() { + var selections = $('#stock-table').bootstrapTable('getSelections'); + + var items = []; + + selections.forEach(function(item) { + items.push(item.pk); + }); + + // Request available labels from the server + inventreeGet( + '{% url "api-stockitem-label-list" %}', + { + enabled: true, + items: items, + }, + { + success: function(response) { + + if (response.length == 0) { + showAlertDialog( + '{% trans "No Labels Found" %}', + '{% trans "No labels found which match selected stock item(s)" %}', + ); + return; + } + + var label = selectLabel(response); + } + } + ); + }); + $('#multi-item-stocktake').click(function() { stockAdjustment('count'); }); diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index 51f7c277db..220abc7dc7 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -17,6 +17,7 @@