mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-16 17:56:30 +00:00
Label sheet printer (#5883)
* Add skeleton for new label sheet plugin * Add custom printing options serializer * Render individual label outputs to HTML * Extract page size and column size * Check label dimensions before printing * Split labels into multiple pages / sheets * Render out multiple labels onto a single sheet * Cleanup base label template - Allow @page style to *not* be generated - Pass through as optional context variable - Check that it still works for single label printing (default behaviour unchanged) - Prevents multiple @page styles from being generated on label sheet output * Fix stylesheets for part labels * Cleanup stock location labels * Cleanup more label templates * Check if label can actually fit on page * Generate output to PDF and return correct response * Update panel.md * Fix unit tests * More unit test fixes
This commit is contained in:
@@ -221,6 +221,10 @@ class LabelPrintMixin(LabelFilterMixin):
|
||||
# Label template
|
||||
label = self.get_object()
|
||||
|
||||
# Check the label dimensions
|
||||
if label.width <= 0 or label.height <= 0:
|
||||
raise ValidationError('Label has invalid dimensions')
|
||||
|
||||
# if the plugin returns a serializer, validate the data
|
||||
if serializer := plugin.get_printing_options_serializer(request, data=request.data):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
@@ -191,10 +191,38 @@ class LabelTemplate(MetadataMixin, models.Model):
|
||||
|
||||
return template_string.render(context)
|
||||
|
||||
def context(self, request):
|
||||
"""Provides context data to the template."""
|
||||
def generate_page_style(self, **kwargs):
|
||||
"""Generate @page style for the label template.
|
||||
|
||||
This is inserted at the top of the style block for a given label
|
||||
"""
|
||||
|
||||
width = kwargs.get('width', self.width)
|
||||
height = kwargs.get('height', self.height)
|
||||
margin = kwargs.get('margin', 0)
|
||||
|
||||
return f"""
|
||||
@page {{
|
||||
size: {width}mm {height}mm;
|
||||
margin: {margin}mm;
|
||||
}}
|
||||
"""
|
||||
|
||||
def context(self, request, **kwargs):
|
||||
"""Provides context data to the template.
|
||||
|
||||
Arguments:
|
||||
request: The HTTP request object
|
||||
kwargs: Additional keyword arguments
|
||||
"""
|
||||
|
||||
context = self.get_context_data(request)
|
||||
|
||||
# By default, each label is supplied with '@page' data
|
||||
# However, it can be excluded, e.g. when rendering a label sheet
|
||||
if kwargs.get('insert_page_style', True):
|
||||
context['page_style'] = self.generate_page_style()
|
||||
|
||||
# Add "basic" context data which gets passed to every label
|
||||
context['base_url'] = get_base_url(request=request)
|
||||
context['date'] = datetime.datetime.now().date()
|
||||
@@ -213,18 +241,31 @@ class LabelTemplate(MetadataMixin, models.Model):
|
||||
|
||||
return context
|
||||
|
||||
def render_as_string(self, request, **kwargs):
|
||||
"""Render the label to a HTML string.
|
||||
def render_as_string(self, request, target_object=None, **kwargs):
|
||||
"""Render the label to a HTML string"""
|
||||
|
||||
Useful for debug mode (viewing generated code)
|
||||
"""
|
||||
return render_to_string(self.template_name, self.context(request), request)
|
||||
if target_object:
|
||||
self.object_to_print = target_object
|
||||
|
||||
def render(self, request, **kwargs):
|
||||
context = self.context(request, **kwargs)
|
||||
|
||||
return render_to_string(
|
||||
self.template_name,
|
||||
context,
|
||||
request
|
||||
)
|
||||
|
||||
def render(self, request, target_object=None, **kwargs):
|
||||
"""Render the label template to a PDF file.
|
||||
|
||||
Uses django-weasyprint plugin to render HTML template
|
||||
"""
|
||||
|
||||
if target_object:
|
||||
self.object_to_print = target_object
|
||||
|
||||
context = self.context(request, **kwargs)
|
||||
|
||||
wp = WeasyprintLabelMixin(
|
||||
request,
|
||||
self.template_name,
|
||||
@@ -235,7 +276,7 @@ class LabelTemplate(MetadataMixin, models.Model):
|
||||
)
|
||||
|
||||
return wp.render_to_response(
|
||||
self.context(request),
|
||||
context,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
@@ -16,9 +16,9 @@ Refer to the documentation for a full list of available template variables.
|
||||
}
|
||||
|
||||
.qr {
|
||||
position: absolute;
|
||||
height: 28mm;
|
||||
width: 28mm;
|
||||
position: relative;
|
||||
top: 0mm;
|
||||
right: 0mm;
|
||||
float: right;
|
||||
|
@@ -4,15 +4,18 @@
|
||||
|
||||
<head>
|
||||
<style>
|
||||
@page {
|
||||
{% localize off %}
|
||||
size: {{ width }}mm {{ height }}mm;
|
||||
{% endlocalize %}
|
||||
{% block margin %}
|
||||
margin: 0mm;
|
||||
{% endblock margin %}
|
||||
}
|
||||
|
||||
{% block page_style %}
|
||||
{% if page_style %}
|
||||
/* @page styling */
|
||||
{% localize off %}
|
||||
{{ page_style }}
|
||||
{% endlocalize %}
|
||||
{% endif %}
|
||||
{% endblock page_style %}
|
||||
|
||||
{% block body_style %}
|
||||
/* body styling */
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
margin: 0mm;
|
||||
@@ -21,20 +24,27 @@
|
||||
page-break-before: always;
|
||||
page-break-after: always;
|
||||
}
|
||||
{% endblock body_style %}
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
/* Global content wrapper div which takes up entire page area */
|
||||
.content {
|
||||
width: 100%;
|
||||
{% localize off %}
|
||||
width: {{ width }}mm;
|
||||
height: {{ height }}mm;
|
||||
{% endlocalize %}
|
||||
break-after: always;
|
||||
position: relative;
|
||||
left: 0mm;
|
||||
top: 0mm;
|
||||
}
|
||||
|
||||
{% block style %}
|
||||
/* User-defined styles can go here */
|
||||
/* User-defined styles can go here, and override any styles defined above */
|
||||
{% endblock style %}
|
||||
|
||||
</style>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
{% block style %}
|
||||
|
||||
.qr {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
left: 0mm;
|
||||
top: 0mm;
|
||||
{% localize off %}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
.part {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
display: inline;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
{% localize off %}
|
||||
left: {{ height }}mm;
|
||||
|
@@ -5,7 +5,7 @@
|
||||
{% block style %}
|
||||
|
||||
.qr {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
left: 0mm;
|
||||
top: 0mm;
|
||||
{% localize off %}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
.part {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
display: inline;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
{% localize off %}
|
||||
left: {{ height }}mm;
|
||||
|
@@ -5,7 +5,7 @@
|
||||
{% block style %}
|
||||
|
||||
.qr {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
left: 0mm;
|
||||
top: 0mm;
|
||||
{% localize off %}
|
||||
|
@@ -5,7 +5,7 @@
|
||||
{% block style %}
|
||||
|
||||
.qr {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
left: 0mm;
|
||||
top: 0mm;
|
||||
{% localize off %}
|
||||
@@ -17,6 +17,5 @@
|
||||
{% endblock style %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<img class='qr' alt="{% trans 'QR Code' %}" src='{% qrcode qr_data %}'>
|
||||
{% endblock content %}
|
||||
|
@@ -5,7 +5,7 @@
|
||||
{% block style %}
|
||||
|
||||
.qr {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
left: 0mm;
|
||||
top: 0mm;
|
||||
{% localize off %}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
.loc {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
display: inline;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
{% localize off %}
|
||||
left: {{ height }}mm;
|
||||
|
Reference in New Issue
Block a user