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:
parent
caf4c293d9
commit
da715d7381
@ -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': {
|
||||||
|
@ -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 = [
|
||||||
|
@ -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]'),
|
||||||
),
|
),
|
||||||
]
|
]
|
@ -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
|
|
||||||
|
28
InvenTree/label/templates/label/label_base.html
Normal file
28
InvenTree/label/templates/label/label_base.html
Normal 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>
|
@ -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))
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user