diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 82c8a99507..fbced0fda7 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -124,6 +124,19 @@ DATABASES = { } } +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + }, + 'qr-code': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'qr-code-cache', + 'TIMEOUT': 3600 + } +} + +QR_CODE_CACHE_ALIAS = 'qr-code' + # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index ccc828de55..4038c2870a 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -8,6 +8,7 @@ Passes URL lookup downstream to each app as required. from django.conf.urls import url, include from django.contrib import admin from django.contrib.auth import views as auth_views +from qr_code import urls as qr_code_urls from company.urls import company_urls @@ -62,6 +63,8 @@ urlpatterns = [ url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html'), name='logout'), url(r'^admin/', admin.site.urls, name='inventree-admin'), + url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')), + url(r'^index/', IndexView.as_view(), name='index'), url(r'^search/', SearchView.as_view(), name='search'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index e727063f0f..ca72dd2b36 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -12,7 +12,7 @@ from django.template.loader import render_to_string from django.http import JsonResponse from django.views import View -from django.views.generic import UpdateView, CreateView, DeleteView +from django.views.generic import UpdateView, CreateView, DeleteView, DetailView from django.views.generic.base import TemplateView from part.models import Part @@ -144,6 +144,43 @@ class AjaxView(AjaxMixin, View): return self.renderJsonResponse(request) +class QRCodeView(AjaxView): + """ An 'AJAXified' view for displaying a QR code. + + Subclasses should implement the get_qr_data(self) function. + """ + + ajax_template_name = "qr_code.html" + + def get(self, request, *args, **kwargs): + self.request = request + self.pk = self.kwargs['pk'] + return self.renderJsonResponse(request, None, context=self.get_context_data()) + + def get_qr_data(self): + """ Returns the text object to render to a QR code. + The actual rendering will be handled by the template """ + + return None + + def get_context_data(self): + """ Get context data for passing to the rendering template. + + Explicity passes the parameter 'qr_data' + """ + + context = {} + + qr = self.get_qr_data() + + if qr: + context['qr_data'] = qr + else: + context['error_msg'] = 'Error generating QR code' + + return context + + class AjaxCreateView(AjaxMixin, CreateView): """ An 'AJAXified' CreateView for creating a new object in the db diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 9f5d9f0e29..e2823ba93b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -180,7 +180,6 @@ class Part(models.Model): def __str__(self): return "{n} - {d}".format(n=self.name, d=self.description) - @property def format_barcode(self): """ Return a JSON string for formatting a barcode for this Part object """ diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 56acbd56b1..789a40b166 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -1,6 +1,5 @@ {% extends "part/part_base.html" %} {% load static %} -{% load qr_code %} {% block details %} {% include 'part/tabs.html' with tab='detail' %} @@ -24,6 +23,7 @@ {% else %}
  • Activate
  • {% endif %} +
  • Show QR Code
  • @@ -116,8 +116,6 @@ {% endif %} -{% qr_from_text part.format_barcode size="s" image_format="png" error_correction="L" %} - {% endblock %} {% block js_load %} @@ -128,6 +126,15 @@ {% block js_ready %} {{ block.super }} + + $("#show-qr-code").click(function() { + launchModalForm( + "{% url 'part-qr' part.id %}", + { + no_post: true, + } + ); + }); $("#duplicate-part").click(function() { launchModalForm( diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index a1b4ddc2d1..de1ab2c6e0 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -22,9 +22,7 @@

    {{ part.name }}{% if part.active == False %} - INACTIVE{% endif %}

    - {% if part.description %}

    {{ part.description }}

    - {% endif %} {% if part.IPN %} IPN diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 401a038193..4cda10a0f0 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -34,8 +34,9 @@ part_attachment_urls = [ part_detail_urls = [ url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), - url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), + + url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'), url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'), @@ -43,6 +44,8 @@ part_detail_urls = [ url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'), url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), + + url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), url(r'^thumbnail/?', views.PartImage.as_view(), name='part-image'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 2c67df7153..d14827a446 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -27,6 +27,7 @@ from .forms import BomExportForm from .forms import EditSupplierPartForm from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView +from InvenTree.views import QRCodeView from InvenTree.helpers import DownloadFile, str2bool @@ -234,6 +235,21 @@ class PartDetail(DetailView): return context +class PartQRCode(QRCodeView): + """ View for displaying a QR code for a Part object """ + + ajax_form_title = "Part QR Code" + + def get_qr_data(self): + """ Generate QR code data for the Part """ + + try: + part = Part.objects.get(id=self.pk) + return part.format_barcode() + except Part.DoesNotExist: + return None + + class PartImage(AjaxUpdateView): """ View for uploading Part image """ diff --git a/InvenTree/static/script/inventree/modals.js b/InvenTree/static/script/inventree/modals.js index b331343c3e..bc7228063f 100644 --- a/InvenTree/static/script/inventree/modals.js +++ b/InvenTree/static/script/inventree/modals.js @@ -73,6 +73,17 @@ function afterForm(response, options) { } } +function modalShowSubmitButton(modal, show=true) { + /* Show (or hide) the 'Submit' button for the given modal form + */ + + if (show) { + $(modal).find('#modal-form-submit').show(); + } else { + $(modal).find('#modal-form-submit').hide(); + } +} + function modalEnable(modal, enable=true) { /* Enable (or disable) modal form elements to prevent user input @@ -444,6 +455,14 @@ function launchModalForm(url, options = {}) { * an object called 'html_form' * * If the request is NOT successful, displays an appropriate error message. + * + * options: + * + * modal - Name of the modal (default = '#modal-form') + * data - Data to pass through to the AJAX request to fill the form + * submit_text - Text for the submit button (default = 'Submit') + * close_text - Text for the close button (default = 'Close') + * no_post - If true, only display form data, hide submit button, and disallow POST */ var modal = options.modal || '#modal-form'; @@ -475,7 +494,13 @@ function launchModalForm(url, options = {}) { if (response.html_form) { injectModalForm(modal, response.html_form); - handleModalForm(url, options); + + if (options.no_post) { + modalShowSubmitButton(modal, false); + } else { + modalShowSubmitButton(modal, true); + handleModalForm(url, options); + } } else { $(modal).modal('hide'); diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4643ae904c..2c16fd079c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -36,7 +36,6 @@ class StockLocation(InvenTreeTree): def has_items(self): return self.stock_items.count() > 0 - @property def format_barcode(self): """ Return a JSON string for formatting a barcode for this StockLocation object """ @@ -139,7 +138,6 @@ class StockItem(models.Model): ('part', 'serial'), ] - @property def format_barcode(self): """ Return a JSON string for formatting a barcode for this StockItem. Can be used to perform lookup of a stockitem using barcode diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 30b7902886..ca38ea29ef 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -2,8 +2,6 @@ {% load static %} {% block content %} -{% load qr_code %} -

    Stock Item Details

    @@ -25,6 +23,8 @@
  • Stocktake
  • {% endif %}
  • Delete stock item
  • +
    +
  • Show QR code
  • @@ -109,9 +109,6 @@ {% endif %}
    -
    - {% qr_from_text item.format_barcode size="s" image_format="png" error_correction="L" %} -
    @@ -148,6 +145,13 @@ }); }); + $("#item-qr-code").click(function() { + launchModalForm("{% url 'stock-item-qr' item.id %}", + { + no_post: true, + }); + }); + {% if item.in_stock %} $("#stock-move").click(function() { launchModalForm( diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 66d1fd2179..20769669a2 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -1,6 +1,5 @@ {% extends "stock/stock_app_base.html" %} {% load static %} -{% load qr_code %} {% block content %}
    @@ -24,12 +23,13 @@
    - {% qr_from_text location.format_barcode size="s" image_format="png" error_correction="L" %} {% endif %} - - + + @@ -101,6 +101,13 @@ return false; }); + $('#location-qr-code').click(function() { + launchModalForm("{% url 'stock-location-qr' location.id %}", + { + no_post: true, + }); + }); + {% endif %} $('#item-create').click(function () { @@ -170,5 +177,4 @@ }, url: "{% url 'api-stock-list' %}", }); - {% endblock %} diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 6b3fc0ff2d..37e54750de 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -10,6 +10,7 @@ from . import views stock_location_detail_urls = [ url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'), url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'), + url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'), # Anything else url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'), @@ -20,6 +21,7 @@ stock_item_detail_urls = [ url(r'^delete/?', views.StockItemDelete.as_view(), name='stock-item-delete'), url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'), url(r'^stocktake/?', views.StockItemStocktake.as_view(), name='stock-item-stocktake'), + url(r'^qr_code/?', views.StockItemQRCode.as_view(), name='stock-item-qr'), url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'), ] diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 7a9513c8c8..c0d1e0dc9d 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -10,6 +10,7 @@ from django.forms.models import model_to_dict from django.forms import HiddenInput from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView +from InvenTree.views import QRCodeView from part.models import Part from .models import StockItem, StockLocation, StockItemTracking @@ -75,6 +76,34 @@ class StockLocationEdit(AjaxUpdateView): ajax_form_title = 'Edit Stock Location' +class StockLocationQRCode(QRCodeView): + """ View for displaying a QR code for a StockLocation object """ + + ajax_form_title = "Stock Location QR code" + + def get_qr_data(self): + """ Generate QR code data for the StockLocation """ + try: + loc = StockLocation.objects.get(id=self.pk) + return loc.format_barcode() + except StockLocation.DoesNotExist: + return None + + +class StockItemQRCode(QRCodeView): + """ View for displaying a QR code for a StockItem object """ + + ajax_form_title = "Stock Item QR Code" + + def get_qr_data(self): + """ Generate QR code data for the StockItem """ + try: + item = StockItem.objects.get(id=self.pk) + return item.format_barcode() + except StockItem.DoesNotExist: + return None + + class StockItemEdit(AjaxUpdateView): """ View for editing details of a single StockItem diff --git a/InvenTree/templates/qr_code.html b/InvenTree/templates/qr_code.html new file mode 100644 index 0000000000..9c0c698368 --- /dev/null +++ b/InvenTree/templates/qr_code.html @@ -0,0 +1,10 @@ +{% load qr_code %} + +
    + {% if qr_data %} + QR Code + {% else %} + Error:
    + {{ error_msg }} + {% endif %} +
    \ No newline at end of file