Refactoring label printing

This commit is contained in:
Oliver Walters 2021-02-22 16:12:13 +11:00
parent caf4c293d9
commit da715d7381
7 changed files with 228 additions and 122 deletions

View File

@ -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': {

View File

@ -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 = [

View File

@ -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]'),
),
]

View File

@ -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),
}

View File

@ -0,0 +1,28 @@
{% load report %}
{% load barcode %}
<head>
<style>
@page {
size: {{ width }}mm {{ height }}mm;
{% block margin %}
margin: 0mm;
{% endblock %}
}
img {
display: inline-block;
image-rendering: pixelated;
}
{% block style %}
{% endblock %}
</style>
</head>
<body>
{% block content %}
<!-- Label data rendered here! -->
{% endblock %}
</body>

View File

@ -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))

View File

@ -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.