2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 20:16:44 +00:00

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'), os.path.join(BASE_DIR, 'templates'),
# Allow templates in the reporting directory to be accessed # Allow templates in the reporting directory to be accessed
os.path.join(MEDIA_ROOT, 'report'), os.path.join(MEDIA_ROOT, 'report'),
os.path.join(MEDIA_ROOT, 'label'),
], ],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {

View File

@ -6,6 +6,7 @@ import sys
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf.urls import url, include from django.conf.urls import url, include
from django.core.exceptions import ValidationError, FieldError from django.core.exceptions import ValidationError, FieldError
from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
@ -13,6 +14,7 @@ from rest_framework import generics, filters
from rest_framework.response import Response from rest_framework.response import Response
import InvenTree.helpers import InvenTree.helpers
import common.models
from stock.models import StockItem, StockLocation 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: class StockItemLabelMixin:
""" """
Mixin for extracting stock items from query params Mixin for extracting stock items from query params
@ -158,7 +228,7 @@ class StockItemLabelDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = StockItemLabelSerializer serializer_class = StockItemLabelSerializer
class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin): class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin, LabelPrintMixin):
""" """
API endpoint for printing a StockItemLabel object API endpoint for printing a StockItemLabel object
""" """
@ -173,34 +243,7 @@ class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin):
items = self.get_items() items = self.get_items()
if len(items) == 0: return self.print(request, items)
# 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'
)
class StockLocationLabelMixin: class StockLocationLabelMixin:
@ -320,7 +363,7 @@ class StockLocationLabelDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = StockLocationLabelSerializer serializer_class = StockLocationLabelSerializer
class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin): class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin, LabelPrintMixin):
""" """
API endpoint for printing a StockLocationLabel object API endpoint for printing a StockLocationLabel object
""" """
@ -332,35 +375,7 @@ class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin)
locations = self.get_locations() locations = self.get_locations()
if len(locations) == 0: return self.print(request, locations)
# 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'
)
label_api_urls = [ 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 import django.core.validators
from django.db import migrations, models from django.db import migrations, models
@ -13,22 +13,22 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='stockitemlabel', model_name='stockitemlabel',
name='length', name='height',
field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), field=models.FloatField(default=20, help_text='Label height, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Height [mm]'),
), ),
migrations.AddField( migrations.AddField(
model_name='stockitemlabel', model_name='stockitemlabel',
name='width', 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( migrations.AddField(
model_name='stocklocationlabel', model_name='stocklocationlabel',
name='length', name='height',
field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), field=models.FloatField(default=20, help_text='Label height, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Height [mm]'),
), ),
migrations.AddField( migrations.AddField(
model_name='stocklocationlabel', model_name='stocklocationlabel',
name='width', 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 os
import io import io
import logging
import datetime
from blabel import LabelWriter from django.conf import settings
from django.db import models from django.db import models
from django.core.validators import FileExtensionValidator, MinValueValidator from django.core.validators import FileExtensionValidator, MinValueValidator
from django.core.exceptions import ValidationError, FieldError from django.core.exceptions import ValidationError, FieldError
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from InvenTree.helpers import validateFilterString, normalize from InvenTree.helpers import validateFilterString, normalize
import common.models
import stock.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): def rename_label(instance, filename):
""" Place the label file into the correct subdirectory """ """ Place the label file into the correct subdirectory """
@ -43,6 +57,21 @@ def validate_stock_location_filters(filters):
return 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): class LabelTemplate(models.Model):
""" """
Base class for generic, filterable labels. 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 # Each class of label files will be stored in a separate subdirectory
SUBDIR = "label" SUBDIR = "label"
# Object we will be printing against (will be filled out later)
object_to_print = None
@property @property
def template(self): def template(self):
@ -92,52 +124,90 @@ class LabelTemplate(models.Model):
help_text=_('Label template is enabled'), 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( width = models.FloatField(
default=10, default=50,
verbose_name=('Width [mm]'), verbose_name=('Width [mm]'),
help_text=_('Label width, specified in mm'), help_text=_('Label width, specified in mm'),
validators=[MinValueValidator(2)] 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) def context(self, request):
writer.write_labels(records, filename)
def render(self, items, **kwargs):
""" """
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): class StockItemLabel(LabelTemplate):
@ -171,29 +241,24 @@ class StockItemLabel(LabelTemplate):
return items.exists() return items.exists()
def get_record_data(self, items): def get_context_data(self, request):
""" """
Generate context data for each provided StockItem Generate context data for each provided StockItem
""" """
records = []
for item in items:
# Add some basic information stock_item = self.object_to_print
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()
})
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): class StockLocationLabel(LabelTemplate):
@ -226,17 +291,14 @@ class StockLocationLabel(LabelTemplate):
return locs.exists() return locs.exists()
def get_record_data(self, locations): def get_context_data(self, request):
""" """
Generate context data for each provided StockLocation Generate context data for each provided StockLocation
""" """
records = [] location = self.object_to_print
for loc in locations:
records.append({ return {
'location': loc, 'location': location,
}) 'qr_data': location.format_barcode(brief=True),
}
return records

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 report.object_to_print = item
if debug_mode: if debug_mode:
outputs.append(report.render_to_string(request)) outputs.append(report.render_as_string(request))
else: else:
outputs.append(report.render(request)) outputs.append(report.render(request))

View File

@ -221,7 +221,7 @@ class ReportTemplateBase(ReportBase):
return context return context
def render_to_string(self, request, **kwargs): def render_as_string(self, request, **kwargs):
""" """
Render the report to a HTML stiring. Render the report to a HTML stiring.