From a742df2c123345f9617bfecd5c64b783efb2c075 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 10:10:58 +1100 Subject: [PATCH 01/11] Adds 'length' and 'width' fields to label models --- .../migrations/0006_auto_20210222_0952.py | 34 +++++++++++++++++++ InvenTree/label/models.py | 16 ++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 InvenTree/label/migrations/0006_auto_20210222_0952.py diff --git a/InvenTree/label/migrations/0006_auto_20210222_0952.py b/InvenTree/label/migrations/0006_auto_20210222_0952.py new file mode 100644 index 0000000000..62e9d1a7f5 --- /dev/null +++ b/InvenTree/label/migrations/0006_auto_20210222_0952.py @@ -0,0 +1,34 @@ +# Generated by Django 3.0.7 on 2021-02-21 22:52 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('label', '0005_auto_20210113_2302'), + ] + + operations = [ + migrations.AddField( + model_name='stockitemlabel', + name='length', + field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), + ), + migrations.AddField( + model_name='stockitemlabel', + name='width', + field=models.FloatField(default=10, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), + ), + migrations.AddField( + model_name='stocklocationlabel', + name='length', + field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), + ), + migrations.AddField( + model_name='stocklocationlabel', + name='width', + field=models.FloatField(default=10, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), + ), + ] diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index 9a98810d28..ae00bc0dc1 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -11,7 +11,7 @@ import io from blabel import LabelWriter from django.db import models -from django.core.validators import FileExtensionValidator +from django.core.validators import FileExtensionValidator, MinValueValidator from django.core.exceptions import ValidationError, FieldError from django.utils.translation import gettext_lazy as _ @@ -92,6 +92,20 @@ class LabelTemplate(models.Model): help_text=_('Label template is enabled'), ) + length = models.FloatField( + default=20, + verbose_name=_('Length [mm]'), + help_text=_('Label length, specified in mm'), + validators=[MinValueValidator(2)] + ) + + width = models.FloatField( + default=10, + verbose_name=('Width [mm]'), + help_text=_('Label width, specified in mm'), + validators=[MinValueValidator(2)] + ) + def get_record_data(self, items): """ Return a list of dict objects, one for each item. From 828b3adc49cdf4791a02cefc89384593b674a8c3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 14:40:40 +1100 Subject: [PATCH 02/11] Update requirements.txt - Remove blabel - Remove django-qr-code - Add python-barcode - Add qrcode --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index e0fdd6dfd9..cf05148ebf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ wheel>=0.34.2 # Wheel Django==3.0.7 # Django package pillow==7.1.0 # Image manipulation -blabel==0.1.3 # Simple PDF label printing djangorestframework==3.10.3 # DRF framework django-dbbackup==3.3.0 # Database backup / restore functionality django-cors-headers==3.2.0 # CORS headers extension for DRF @@ -16,7 +15,6 @@ tablib==0.13.0 # Import / export data files django-crispy-forms==1.8.1 # Form helpers django-import-export==2.0.0 # Data import / export for admin interface django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files -django-qr-code==1.2.0 # Generate QR codes flake8==3.8.3 # PEP checking pep8-naming==0.11.1 # PEP naming convention extension coverage==5.3 # Unit test coverage @@ -31,5 +29,7 @@ certifi # Certifi is (most likely) installed through one django-error-report==0.2.0 # Error report viewer for the admin interface django-test-migrations==1.1.0 # Unit testing for database migrations django-migration-linter==2.5.0 # Linting checks for database migrations +python-barcode[images]==0.13.1 # Barcode generator +qrcode[pil]==6.1 # QR code generator inventree # Install the latest version of the InvenTree API python library From 23da591c2201fe964029d6125c9e7ce2d75f30fd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 15:00:12 +1100 Subject: [PATCH 03/11] Remove old dependency on django_qr_code --- InvenTree/InvenTree/settings.py | 3 -- InvenTree/InvenTree/urls.py | 3 -- InvenTree/report/templatetags/barcode.py | 64 ++++++++++++++++++++++++ InvenTree/templates/qr_code.html | 10 ++-- 4 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 InvenTree/report/templatetags/barcode.py diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 7e3ed2c7a6..0ab85a16cf 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -204,7 +204,6 @@ INSTALLED_APPS = [ 'crispy_forms', # Improved form rendering 'import_export', # Import / export tables to file 'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files - 'qr_code', # Generate QR codes 'mptt', # Modified Preorder Tree Traversal 'markdownx', # Markdown editing 'markdownify', # Markdown template rendering @@ -407,8 +406,6 @@ CACHES = { } } -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 1df5e83d5d..9f7be3fce1 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -9,7 +9,6 @@ from django.conf.urls import url, include from django.urls import path 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 from company.urls import supplier_part_urls @@ -141,8 +140,6 @@ urlpatterns = [ url(r'^admin/shell/', include('django_admin_shell.urls')), 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'), url(r'^stats/', DatabaseStatsView.as_view(), name='stats'), diff --git a/InvenTree/report/templatetags/barcode.py b/InvenTree/report/templatetags/barcode.py new file mode 100644 index 0000000000..18afdea81e --- /dev/null +++ b/InvenTree/report/templatetags/barcode.py @@ -0,0 +1,64 @@ +""" +Template tags for rendering various barcodes +""" + +import os +import base64 + +from io import BytesIO + + +from django import template + +import qrcode +import barcode + +register = template.Library() + + +def image_data(img, fmt='PNG'): + """ + Convert an image into HTML renderable data + + Returns a string ``data:image/FMT;base64,xxxxxxxxx`` which can be rendered to an tag + """ + + buffered = BytesIO() + img.save(buffered, format=fmt) + + img_str = base64.b64encode(buffered.getvalue()) + + return f"data:image/{fmt.lower()};charset=utf-8;base64," + img_str.decode() + + +@register.simple_tag() +def qr_code(data, **kwargs): + """ + Return a byte-encoded QR code image + + Optional kwargs + --------------- + + fill_color: Fill color (default = black) + back_color: Background color (default = white) + """ + + # Construct "default" values + params = dict( + box_size=20, + border=1, + ) + + fill_color = kwargs.pop('fill_color', 'black') + back_color = kwargs.pop('back_color', 'white') + + params.update(**kwargs) + + qr = qrcode.QRCode(**params) + + qr.add_data(data, optimize=20) + qr.make(fit=True) + + qri = qr.make_image(fill_color=fill_color, back_color=back_color) + + return image_data(qri) \ No newline at end of file diff --git a/InvenTree/templates/qr_code.html b/InvenTree/templates/qr_code.html index 8bb288f797..c6c2f44b87 100644 --- a/InvenTree/templates/qr_code.html +++ b/InvenTree/templates/qr_code.html @@ -1,12 +1,14 @@ -{% load qr_code %} +{% load barcode %} +{% load i18n %}
{% if qr_data %}
- QR Code +
{% else %} - Error:
- {{ error_msg }} +
+ {% trans "QR data not provided" %} +
{% endif %}
\ No newline at end of file From caf4c293d991868e259e67f77bcd1337ef5569fe Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 15:15:25 +1100 Subject: [PATCH 04/11] Rename "barcode" module to "barcodes" to prevent import shadowing - Add 'barcode' support --- InvenTree/InvenTree/static/css/inventree.css | 1 + InvenTree/InvenTree/urls.py | 2 +- InvenTree/{barcode => barcodes}/__init__.py | 0 InvenTree/{barcode => barcodes}/api.py | 2 +- InvenTree/{barcode => barcodes}/barcode.py | 2 +- .../plugins/digikey_barcode.py | 2 +- .../plugins/inventree_barcode.py | 2 +- InvenTree/{barcode => barcodes}/tests.py | 0 InvenTree/report/templatetags/barcode.py | 30 +++++++++++++++---- InvenTree/templates/qr_code.html | 2 +- 10 files changed, 32 insertions(+), 11 deletions(-) rename InvenTree/{barcode => barcodes}/__init__.py (100%) rename InvenTree/{barcode => barcodes}/api.py (99%) rename InvenTree/{barcode => barcodes}/barcode.py (98%) rename InvenTree/{barcode => barcodes}/plugins/digikey_barcode.py (85%) rename InvenTree/{barcode => barcodes}/plugins/inventree_barcode.py (98%) rename InvenTree/{barcode => barcodes}/tests.py (100%) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index a0053e5f53..34c101e86d 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -125,6 +125,7 @@ .qr-container { width: 100%; align-content: center; + object-fit: fill; } .navbar-brand { diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 9f7be3fce1..c5b439c0be 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -20,7 +20,7 @@ from stock.urls import stock_urls from build.urls import build_urls from order.urls import order_urls -from barcode.api import barcode_api_urls +from barcodes.api import barcode_api_urls from common.api import common_api_urls from part.api import part_api_urls, bom_api_urls from company.api import company_api_urls diff --git a/InvenTree/barcode/__init__.py b/InvenTree/barcodes/__init__.py similarity index 100% rename from InvenTree/barcode/__init__.py rename to InvenTree/barcodes/__init__.py diff --git a/InvenTree/barcode/api.py b/InvenTree/barcodes/api.py similarity index 99% rename from InvenTree/barcode/api.py rename to InvenTree/barcodes/api.py index f8a2c37329..d727f5a778 100644 --- a/InvenTree/barcode/api.py +++ b/InvenTree/barcodes/api.py @@ -12,7 +12,7 @@ from rest_framework.views import APIView from stock.models import StockItem from stock.serializers import StockItemSerializer -from barcode.barcode import load_barcode_plugins, hash_barcode +from barcodes.barcode import load_barcode_plugins, hash_barcode class BarcodeScan(APIView): diff --git a/InvenTree/barcode/barcode.py b/InvenTree/barcodes/barcode.py similarity index 98% rename from InvenTree/barcode/barcode.py rename to InvenTree/barcodes/barcode.py index 004182e0be..412065bf75 100644 --- a/InvenTree/barcode/barcode.py +++ b/InvenTree/barcodes/barcode.py @@ -5,7 +5,7 @@ import hashlib import logging from InvenTree import plugins as InvenTreePlugins -from barcode import plugins as BarcodePlugins +from barcodes import plugins as BarcodePlugins from stock.models import StockItem from stock.serializers import StockItemSerializer, LocationSerializer diff --git a/InvenTree/barcode/plugins/digikey_barcode.py b/InvenTree/barcodes/plugins/digikey_barcode.py similarity index 85% rename from InvenTree/barcode/plugins/digikey_barcode.py rename to InvenTree/barcodes/plugins/digikey_barcode.py index 1a0825e03c..656cdfa6f4 100644 --- a/InvenTree/barcode/plugins/digikey_barcode.py +++ b/InvenTree/barcodes/plugins/digikey_barcode.py @@ -4,7 +4,7 @@ DigiKey barcode decoding """ -from barcode.barcode import BarcodePlugin +from barcodes.barcode import BarcodePlugin class DigikeyBarcodePlugin(BarcodePlugin): diff --git a/InvenTree/barcode/plugins/inventree_barcode.py b/InvenTree/barcodes/plugins/inventree_barcode.py similarity index 98% rename from InvenTree/barcode/plugins/inventree_barcode.py rename to InvenTree/barcodes/plugins/inventree_barcode.py index 93fb58cbc0..842f9029aa 100644 --- a/InvenTree/barcode/plugins/inventree_barcode.py +++ b/InvenTree/barcodes/plugins/inventree_barcode.py @@ -13,7 +13,7 @@ references model objects actually exist in the database. import json -from barcode.barcode import BarcodePlugin +from barcodes.barcode import BarcodePlugin from stock.models import StockItem, StockLocation from part.models import Part diff --git a/InvenTree/barcode/tests.py b/InvenTree/barcodes/tests.py similarity index 100% rename from InvenTree/barcode/tests.py rename to InvenTree/barcodes/tests.py diff --git a/InvenTree/report/templatetags/barcode.py b/InvenTree/report/templatetags/barcode.py index 18afdea81e..9b826d8921 100644 --- a/InvenTree/report/templatetags/barcode.py +++ b/InvenTree/report/templatetags/barcode.py @@ -10,8 +10,8 @@ from io import BytesIO from django import template -import qrcode -import barcode +import qrcode as python_qrcode +import barcode as python_barcode register = template.Library() @@ -32,7 +32,7 @@ def image_data(img, fmt='PNG'): @register.simple_tag() -def qr_code(data, **kwargs): +def qrcode(data, **kwargs): """ Return a byte-encoded QR code image @@ -54,11 +54,31 @@ def qr_code(data, **kwargs): params.update(**kwargs) - qr = qrcode.QRCode(**params) + qr = python_qrcode.QRCode(**params) qr.add_data(data, optimize=20) qr.make(fit=True) qri = qr.make_image(fill_color=fill_color, back_color=back_color) - return image_data(qri) \ No newline at end of file + return image_data(qri) + + +@register.simple_tag() +def barcode(data, barcode_class='code128', **kwargs): + """ + Render a barcode + """ + + constructor = python_barcode.get_barcode_class(barcode_class) + + data = str(data).zfill(constructor.digits) + + writer = python_barcode.writer.ImageWriter + + barcode_image = constructor(data, writer=writer()) + + image = barcode_image.render(writer_options=kwargs) + + # Render to byte-encoded PNG + return image_data(image) diff --git a/InvenTree/templates/qr_code.html b/InvenTree/templates/qr_code.html index c6c2f44b87..8964ef02be 100644 --- a/InvenTree/templates/qr_code.html +++ b/InvenTree/templates/qr_code.html @@ -4,7 +4,7 @@
{% if qr_data %}
- +
{% else %}
From da715d738163f09afb3fbdf897a7ea1b3ceae6ab Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 16:12:13 +1100 Subject: [PATCH 05/11] Refactoring label printing --- InvenTree/InvenTree/settings.py | 1 + InvenTree/label/api.py | 133 ++++++++------ ...222_0952.py => 0006_auto_20210222_1535.py} | 14 +- InvenTree/label/models.py | 170 ++++++++++++------ .../label/templates/label/label_base.html | 28 +++ InvenTree/report/api.py | 2 +- InvenTree/report/models.py | 2 +- 7 files changed, 228 insertions(+), 122 deletions(-) rename InvenTree/label/migrations/{0006_auto_20210222_0952.py => 0006_auto_20210222_1535.py} (67%) create mode 100644 InvenTree/label/templates/label/label_base.html diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 0ab85a16cf..70760624c6 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -249,6 +249,7 @@ TEMPLATES = [ os.path.join(BASE_DIR, 'templates'), # Allow templates in the reporting directory to be accessed os.path.join(MEDIA_ROOT, 'report'), + os.path.join(MEDIA_ROOT, 'label'), ], 'APP_DIRS': True, 'OPTIONS': { diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index b2bfe9164f..899b00b0ec 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -6,6 +6,7 @@ import sys from django.utils.translation import ugettext as _ from django.conf.urls import url, include from django.core.exceptions import ValidationError, FieldError +from django.http import HttpResponse from django_filters.rest_framework import DjangoFilterBackend @@ -13,6 +14,7 @@ from rest_framework import generics, filters from rest_framework.response import Response import InvenTree.helpers +import common.models from stock.models import StockItem, StockLocation @@ -40,6 +42,74 @@ class LabelListView(generics.ListAPIView): ] +class LabelPrintMixin: + """ + Mixin for printing labels + """ + + def print(self, request, items_to_print): + """ + Print this label template against a number of pre-validated items + """ + + if len(items_to_print) == 0: + # No valid items provided, return an error message + data = { + 'error': _('No valid objects provided to template'), + } + + return Response(data, status=400) + + outputs = [] + + # In debug mode, generate single HTML output, rather than PDF + debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') + + # Merge one or more PDF files into a single download + for item in items_to_print: + label = self.get_object() + label.object_to_print = item + + if debug_mode: + outputs.append(label.render_as_string(request)) + else: + outputs.append(label.render(request)) + + if debug_mode: + """ + Contatenate all rendered templates into a single HTML string, + and return the string as a HTML response. + """ + + html = "\n".join(outputs) + + return HttpResponse(html) + else: + """ + Concatenate all rendered pages into a single PDF object, + and return the resulting document! + """ + + 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, + 'inventree_label.pdf', + content_type='application/pdf' + ) + + class StockItemLabelMixin: """ Mixin for extracting stock items from query params @@ -158,7 +228,7 @@ class StockItemLabelDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = StockItemLabelSerializer -class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin): +class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin, LabelPrintMixin): """ API endpoint for printing a StockItemLabel object """ @@ -173,34 +243,7 @@ class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin): 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 rendering'), - 'message': str(e), - } - - return Response(data, status=400) - - return InvenTree.helpers.DownloadFile( - pdf.getbuffer(), - 'stock_item_label.pdf', - content_type='application/pdf' - ) + return self.print(request, items) class StockLocationLabelMixin: @@ -320,7 +363,7 @@ class StockLocationLabelDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = StockLocationLabelSerializer -class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin): +class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin, LabelPrintMixin): """ API endpoint for printing a StockLocationLabel object """ @@ -332,35 +375,7 @@ class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin) locations = self.get_locations() - if len(locations) == 0: - # No valid locations provided - return an error message - - return Response( - { - 'error': _('Must provide valid StockLocation(s)'), - }, - status=400, - ) - - label = self.get_object() - - try: - pdf = label.render(locations) - except: - e = sys.exc_info()[1] - - data = { - 'error': _('Error during label rendering'), - 'message': str(e), - } - - return Response(data, status=400) - - return InvenTree.helpers.DownloadFile( - pdf.getbuffer(), - 'stock_location_label.pdf', - content_type='application/pdf' - ) + return self.print(request, locations) label_api_urls = [ diff --git a/InvenTree/label/migrations/0006_auto_20210222_0952.py b/InvenTree/label/migrations/0006_auto_20210222_1535.py similarity index 67% rename from InvenTree/label/migrations/0006_auto_20210222_0952.py rename to InvenTree/label/migrations/0006_auto_20210222_1535.py index 62e9d1a7f5..ea3441b64f 100644 --- a/InvenTree/label/migrations/0006_auto_20210222_0952.py +++ b/InvenTree/label/migrations/0006_auto_20210222_1535.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.7 on 2021-02-21 22:52 +# Generated by Django 3.0.7 on 2021-02-22 04:35 import django.core.validators from django.db import migrations, models @@ -13,22 +13,22 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='stockitemlabel', - name='length', - field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), + name='height', + field=models.FloatField(default=20, help_text='Label height, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Height [mm]'), ), migrations.AddField( model_name='stockitemlabel', name='width', - field=models.FloatField(default=10, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), + field=models.FloatField(default=50, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), ), migrations.AddField( model_name='stocklocationlabel', - name='length', - field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), + name='height', + field=models.FloatField(default=20, help_text='Label height, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Height [mm]'), ), migrations.AddField( model_name='stocklocationlabel', name='width', - field=models.FloatField(default=10, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), + field=models.FloatField(default=50, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), ), ] diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index ae00bc0dc1..d0962655d5 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -7,19 +7,33 @@ from __future__ import unicode_literals import os import io +import logging +import datetime -from blabel import LabelWriter - +from django.conf import settings from django.db import models from django.core.validators import FileExtensionValidator, MinValueValidator from django.core.exceptions import ValidationError, FieldError +from django.template.loader import render_to_string + from django.utils.translation import gettext_lazy as _ from InvenTree.helpers import validateFilterString, normalize +import common.models import stock.models +try: + from django_weasyprint import WeasyTemplateResponseMixin +except OSError as err: + print("OSError: {e}".format(e=err)) + print("You may require some further system packages to be installed.") + sys.exit(1) + + +logger = logging.getLogger(__name__) + def rename_label(instance, filename): """ Place the label file into the correct subdirectory """ @@ -43,6 +57,21 @@ def validate_stock_location_filters(filters): return filters +class WeasyprintLabelMixin(WeasyTemplateResponseMixin): + """ + Class for rendering a label to a PDF + """ + + pdf_filename = 'label.pdf' + pdf_attachment = True + + def __init__(self, request, template, **kwargs): + + self.request = request + self.template_name = template + self.pdf_filename = kwargs.get('filename', 'label.pdf') + + class LabelTemplate(models.Model): """ Base class for generic, filterable labels. @@ -53,6 +82,9 @@ class LabelTemplate(models.Model): # Each class of label files will be stored in a separate subdirectory SUBDIR = "label" + + # Object we will be printing against (will be filled out later) + object_to_print = None @property def template(self): @@ -92,52 +124,90 @@ class LabelTemplate(models.Model): help_text=_('Label template is enabled'), ) - length = models.FloatField( - default=20, - verbose_name=_('Length [mm]'), - help_text=_('Label length, specified in mm'), - validators=[MinValueValidator(2)] - ) - width = models.FloatField( - default=10, + default=50, verbose_name=('Width [mm]'), help_text=_('Label width, specified in mm'), validators=[MinValueValidator(2)] ) - def get_record_data(self, items): + height = models.FloatField( + default=20, + verbose_name=_('Height [mm]'), + help_text=_('Label height, specified in mm'), + validators=[MinValueValidator(2)] + ) + + @property + def template_name(self): """ - Return a list of dict objects, one for each item. + Returns the file system path to the template file. + Required for passing the file to an external process """ - return [] + template = self.label.name + template = template.replace('/', os.path.sep) + template = template.replace('\\', os.path.sep) - def render_to_file(self, filename, items, **kwargs): + template = os.path.join(settings.MEDIA_ROOT, template) + + return template + + def get_context_data(self, request): """ - Render labels to a PDF file + Supply custom context data to the template for rendering. + + Note: Override this in any subclass """ - records = self.get_record_data(items) + return {} - writer = LabelWriter(self.template) - - writer.write_labels(records, filename) - - def render(self, items, **kwargs): + def context(self, request): """ - Render labels to an in-memory PDF object, and return it + Provides context data to the template. """ - records = self.get_record_data(items) + context = self.get_context_data(request) - writer = LabelWriter(self.template) + # Add "basic" context data which gets passed to every label + context['base_url'] = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL') + context['date'] = datetime.datetime.now().date() + context['datetime'] = datetime.datetime.now() + context['request'] = request + context['user'] = request.user + context['width'] = self.width + context['height'] = self.height - buffer = io.BytesIO() + return context - writer.write_labels(records, buffer) + def render_as_string(self, request, **kwargs): + """ + Render the label to a HTML string - return buffer + Useful for debug mode (viewing generated code) + """ + + return render_to_string(self.template_name, self.context(request), request) + + def render(self, request, **kwargs): + """ + Render the label template to a PDF file + + Uses django-weasyprint plugin to render HTML template + """ + + wp = WeasyprintLabelMixin( + request, + self.template_name, + base_url=request.build_absolute_uri("/"), + presentational_hints=True, + **kwargs + ) + + return wp.render_to_response( + self.context(request), + **kwargs + ) class StockItemLabel(LabelTemplate): @@ -171,29 +241,24 @@ class StockItemLabel(LabelTemplate): return items.exists() - def get_record_data(self, items): + def get_context_data(self, request): """ Generate context data for each provided StockItem """ - records = [] - - for item in items: - # Add some basic information - records.append({ - 'item': item, - 'part': item.part, - 'name': item.part.name, - 'ipn': item.part.IPN, - 'quantity': normalize(item.quantity), - 'serial': item.serial, - 'uid': item.uid, - 'pk': item.pk, - 'qr_data': item.format_barcode(brief=True), - 'tests': item.testResultMap() - }) + stock_item = self.object_to_print - return records + return { + 'item': stock_item, + 'part': stock_item.part, + 'name': stock_item.part.full_name, + 'ipn': stock_item.part.IPN, + 'quantity': normalize(stock_item.quantity), + 'serial': stock_item.serial, + 'uid': stock_item.uid, + 'qr_data': stock_item.format_barcode(brief=True), + 'tests': stock_item.testResultMap() + } class StockLocationLabel(LabelTemplate): @@ -226,17 +291,14 @@ class StockLocationLabel(LabelTemplate): return locs.exists() - def get_record_data(self, locations): + def get_context_data(self, request): """ Generate context data for each provided StockLocation """ - records = [] - - for loc in locations: + location = self.object_to_print - records.append({ - 'location': loc, - }) - - return records + return { + 'location': location, + 'qr_data': location.format_barcode(brief=True), + } diff --git a/InvenTree/label/templates/label/label_base.html b/InvenTree/label/templates/label/label_base.html new file mode 100644 index 0000000000..2c564d1132 --- /dev/null +++ b/InvenTree/label/templates/label/label_base.html @@ -0,0 +1,28 @@ +{% load report %} +{% load barcode %} + + + + + + + {% block content %} + + {% endblock %} + diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index dd937f0aba..09bd9fde8f 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -164,7 +164,7 @@ class ReportPrintMixin: report.object_to_print = item if debug_mode: - outputs.append(report.render_to_string(request)) + outputs.append(report.render_as_string(request)) else: outputs.append(report.render(request)) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 00777449fc..abbdea9025 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -221,7 +221,7 @@ class ReportTemplateBase(ReportBase): return context - def render_to_string(self, request, **kwargs): + def render_as_string(self, request, **kwargs): """ Render the report to a HTML stiring. From bf51e1bfb1ba2627f7adfc3ffe152f926dad9d0f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 16:39:38 +1100 Subject: [PATCH 06/11] Update label templates - New ones are automatically copied across now --- InvenTree/label/apps.py | 49 ++++++++++++++++++- .../label/templates/label/stockitem/qr.html | 20 ++++++++ .../templates/label/stocklocation/qr.html | 20 ++++++++ .../label/stocklocation/qr_and_text.html | 33 +++++++++++++ InvenTree/label/templates/stockitem/qr.html | 16 ------ .../label/templates/stocklocation/qr.html | 16 ------ .../templates/stocklocation/qr_and_text.html | 43 ---------------- 7 files changed, 120 insertions(+), 77 deletions(-) create mode 100644 InvenTree/label/templates/label/stockitem/qr.html create mode 100644 InvenTree/label/templates/label/stocklocation/qr.html create mode 100644 InvenTree/label/templates/label/stocklocation/qr_and_text.html delete mode 100644 InvenTree/label/templates/stockitem/qr.html delete mode 100644 InvenTree/label/templates/stocklocation/qr.html delete mode 100644 InvenTree/label/templates/stocklocation/qr_and_text.html diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py index 321047a551..8ec2be24f4 100644 --- a/InvenTree/label/apps.py +++ b/InvenTree/label/apps.py @@ -1,6 +1,7 @@ import os import shutil import logging +import hashlib from django.apps import AppConfig from django.conf import settings @@ -9,6 +10,20 @@ from django.conf import settings logger = logging.getLogger(__name__) +def hashFile(filename): + """ + Calculate the MD5 hash of a file + """ + + md5 = hashlib.md5() + + with open(filename, 'rb') as f: + data = f.read() + md5.update(data) + + return md5.hexdigest() + + class LabelConfig(AppConfig): name = 'label' @@ -35,6 +50,7 @@ class LabelConfig(AppConfig): src_dir = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'templates', + 'label', 'stockitem', ) @@ -70,7 +86,21 @@ class LabelConfig(AppConfig): src_file = os.path.join(src_dir, label['file']) dst_file = os.path.join(settings.MEDIA_ROOT, filename) - if not os.path.exists(dst_file): + to_copy = False + + if os.path.exists(dst_file): + # File already exists - let's see if it is the "same", + # or if we need to overwrite it with a newer copy! + + if not hashFile(dst_file) == hashFile(src_file): + logger.info(f"Hash differs for '{filename}'") + to_copy = True + + else: + logger.info(f"Label template '{filename}' is not present") + to_copy = True + + if to_copy: logger.info(f"Copying label template '{dst_file}'") shutil.copyfile(src_file, dst_file) @@ -106,6 +136,7 @@ class LabelConfig(AppConfig): src_dir = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'templates', + 'label', 'stocklocation', ) @@ -146,7 +177,21 @@ class LabelConfig(AppConfig): src_file = os.path.join(src_dir, label['file']) dst_file = os.path.join(settings.MEDIA_ROOT, filename) - if not os.path.exists(dst_file): + to_copy = False + + if os.path.exists(dst_file): + # File already exists - let's see if it is the "same", + # or if we need to overwrite it with a newer copy! + + if not hashFile(dst_file) == hashFile(src_file): + logger.info(f"Hash differs for '{filename}'") + to_copy = True + + else: + logger.info(f"Label template '{filename}' is not present") + to_copy = True + + if to_copy: logger.info(f"Copying label template '{dst_file}'") shutil.copyfile(src_file, dst_file) diff --git a/InvenTree/label/templates/label/stockitem/qr.html b/InvenTree/label/templates/label/stockitem/qr.html new file mode 100644 index 0000000000..8f489f81b8 --- /dev/null +++ b/InvenTree/label/templates/label/stockitem/qr.html @@ -0,0 +1,20 @@ +{% extends "label/label_base.html" %} + +{% load barcode %} + +{% block style %} + +.qr { + position: fixed; + left: 0mm; + top: 0mm; + height: {{ height }}mm; + width: {{ height }}mm; +} + +{% endblock %} + +{% block content %} + + +{% endblock %} diff --git a/InvenTree/label/templates/label/stocklocation/qr.html b/InvenTree/label/templates/label/stocklocation/qr.html new file mode 100644 index 0000000000..8f489f81b8 --- /dev/null +++ b/InvenTree/label/templates/label/stocklocation/qr.html @@ -0,0 +1,20 @@ +{% extends "label/label_base.html" %} + +{% load barcode %} + +{% block style %} + +.qr { + position: fixed; + left: 0mm; + top: 0mm; + height: {{ height }}mm; + width: {{ height }}mm; +} + +{% endblock %} + +{% block content %} + + +{% endblock %} diff --git a/InvenTree/label/templates/label/stocklocation/qr_and_text.html b/InvenTree/label/templates/label/stocklocation/qr_and_text.html new file mode 100644 index 0000000000..c5756b96f2 --- /dev/null +++ b/InvenTree/label/templates/label/stocklocation/qr_and_text.html @@ -0,0 +1,33 @@ +{% extends "label/label_base.html" %} + +{% load barcode %} + +{% block style %} + +.qr { + position: fixed; + left: 0mm; + top: 0mm; + height: {{ height }}mm; + width: {{ height }}mm; +} + +.loc { + font-family: Arial, Helvetica, sans-serif; + display: inline; + position: absolute; + left: {{ width }}, + top: 2mm; +} + +{% endblock %} + +{% block content %} + + + +
+ {{ location.name }} +
+ +{% endblock %} diff --git a/InvenTree/label/templates/stockitem/qr.html b/InvenTree/label/templates/stockitem/qr.html deleted file mode 100644 index 9cd9d20769..0000000000 --- a/InvenTree/label/templates/stockitem/qr.html +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/InvenTree/label/templates/stocklocation/qr.html b/InvenTree/label/templates/stocklocation/qr.html deleted file mode 100644 index 8fb22fbb40..0000000000 --- a/InvenTree/label/templates/stocklocation/qr.html +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/InvenTree/label/templates/stocklocation/qr_and_text.html b/InvenTree/label/templates/stocklocation/qr_and_text.html deleted file mode 100644 index 9a555aca15..0000000000 --- a/InvenTree/label/templates/stocklocation/qr_and_text.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - -
- {{ location.name }} -
-
-
- Location ID: {{ location.pk }} -
-
- \ No newline at end of file From 7d54997cb84001b13dcb88eb4545fd7cdd79a1d2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 16:48:00 +1100 Subject: [PATCH 07/11] Tweaks --- InvenTree/label/apps.py | 14 ++++++++++++-- .../templates/label/stocklocation/qr_and_text.html | 2 +- InvenTree/templates/js/label.js | 12 ++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py index 8ec2be24f4..9f2d3ea9c4 100644 --- a/InvenTree/label/apps.py +++ b/InvenTree/label/apps.py @@ -70,6 +70,8 @@ class LabelConfig(AppConfig): 'file': 'qr.html', 'name': 'QR Code', 'description': 'Simple QR code label', + 'width': 24, + 'height': 24, }, ] @@ -116,7 +118,9 @@ class LabelConfig(AppConfig): description=label['description'], label=filename, filters='', - enabled=True + enabled=True, + width=label['width'], + height=label['height'], ) except: pass @@ -156,11 +160,15 @@ class LabelConfig(AppConfig): 'file': 'qr.html', 'name': 'QR Code', 'description': 'Simple QR code label', + 'width': 24, + 'height': 24, }, { 'file': 'qr_and_text.html', 'name': 'QR and text', 'description': 'Label with QR code and name of location', + 'width': 50, + 'height': 24, } ] @@ -207,7 +215,9 @@ class LabelConfig(AppConfig): description=label['description'], label=filename, filters='', - enabled=True + enabled=True, + width=label['width'], + height=label['height'], ) except: pass diff --git a/InvenTree/label/templates/label/stocklocation/qr_and_text.html b/InvenTree/label/templates/label/stocklocation/qr_and_text.html index c5756b96f2..2ff54dc817 100644 --- a/InvenTree/label/templates/label/stocklocation/qr_and_text.html +++ b/InvenTree/label/templates/label/stocklocation/qr_and_text.html @@ -16,7 +16,7 @@ font-family: Arial, Helvetica, sans-serif; display: inline; position: absolute; - left: {{ width }}, + left: {{ height }}mm; top: 2mm; } diff --git a/InvenTree/templates/js/label.js b/InvenTree/templates/js/label.js index 63f028fa1d..cbb71c7b08 100644 --- a/InvenTree/templates/js/label.js +++ b/InvenTree/templates/js/label.js @@ -114,6 +114,18 @@ function selectLabel(labels, items, options={}) { * (via AJAX) from the server. */ + // If only a single label template is provided, + // just run with that! + + if (labels.length == 1) { + if (options.success) { + options.success(labels[0].pk); + } + + return; + } + + var modal = options.modal || '#modal-form'; var label_list = makeOptionsList( From 7582b9ccf4723aee4b2aaf91da93aa87e33db474 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 17:02:33 +1100 Subject: [PATCH 08/11] PEP fixes --- InvenTree/label/api.py | 2 -- InvenTree/label/models.py | 2 +- InvenTree/report/templatetags/barcode.py | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 899b00b0ec..a4800cd5d9 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/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 from django.core.exceptions import ValidationError, FieldError diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index d0962655d5..0c6a108f31 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -5,8 +5,8 @@ Label printing models # -*- coding: utf-8 -*- from __future__ import unicode_literals +import sys import os -import io import logging import datetime diff --git a/InvenTree/report/templatetags/barcode.py b/InvenTree/report/templatetags/barcode.py index 9b826d8921..e38fab1f06 100644 --- a/InvenTree/report/templatetags/barcode.py +++ b/InvenTree/report/templatetags/barcode.py @@ -2,12 +2,9 @@ Template tags for rendering various barcodes """ -import os import base64 - from io import BytesIO - from django import template import qrcode as python_qrcode From 9159afe9cefba554e04349afabd167ccfbd5bc2e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 17:27:48 +1100 Subject: [PATCH 09/11] Unit test fix --- InvenTree/stock/test_views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/stock/test_views.py b/InvenTree/stock/test_views.py index 8789e9f76e..261598ae22 100644 --- a/InvenTree/stock/test_views.py +++ b/InvenTree/stock/test_views.py @@ -105,9 +105,6 @@ class StockItemTest(StockViewTestCase): response = self.client.get(reverse('stock-item-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - data = str(response.content) - self.assertIn('Error:', data) - def test_adjust_items(self): url = reverse('stock-adjust') From 28e9aa4afa504fd8739fc888f29524f2341952c3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 18:11:40 +1100 Subject: [PATCH 10/11] More unit fixes --- InvenTree/part/test_views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index 13349aae15..8c60941511 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -270,14 +270,13 @@ class PartQRTest(PartViewTestCase): data = str(response.content) self.assertIn('Part QR Code', data) - self.assertIn(' Date: Mon, 22 Feb 2021 18:44:59 +1100 Subject: [PATCH 11/11] PEP!!! --- InvenTree/part/test_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index 8c60941511..45fd39ceb0 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -275,7 +275,6 @@ class PartQRTest(PartViewTestCase): def test_invalid_part(self): response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') - data = str(response.content) self.assertEqual(response.status_code, 200)