From da715d738163f09afb3fbdf897a7ea1b3ceae6ab Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 16:12:13 +1100 Subject: [PATCH] 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.