2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-11 15:34:15 +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:
Oliver
2023-11-09 09:00:23 +11:00
committed by GitHub
parent e674ca7437
commit a0b1ba62a9
14 changed files with 380 additions and 35 deletions

View File

@ -90,4 +90,5 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
if debug:
return self.render_to_html(label, request, **kwargs)
return self.render_to_pdf(label, request, **kwargs)

View File

@ -0,0 +1,267 @@
"""Label printing plugin which supports printing multiple labels on a single page"""
import logging
import math
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.http import JsonResponse
from django.utils.translation import gettext_lazy as _
import weasyprint
from rest_framework import serializers
import report.helpers
from label.models import LabelOutput, LabelTemplate
from plugin import InvenTreePlugin
from plugin.mixins import LabelPrintingMixin, SettingsMixin
logger = logging.getLogger('inventree')
class LabelPrintingOptionsSerializer(serializers.Serializer):
"""Custom printing options for the label sheet plugin"""
page_size = serializers.ChoiceField(
choices=report.helpers.report_page_size_options(),
default='A4',
label=_('Page Size'),
help_text=_('Page size for the label sheet')
)
border = serializers.BooleanField(
default=False,
label=_('Border'),
help_text=_('Print a border around each label')
)
landscape = serializers.BooleanField(
default=False,
label=_('Landscape'),
help_text=_('Print the label sheet in landscape mode')
)
class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
"""Builtin plugin for label printing.
This plugin arrays multiple labels onto a single larger sheet,
and returns the resulting PDF file.
"""
NAME = "InvenTreeLabelSheet"
TITLE = _("InvenTree Label Sheet Printer")
DESCRIPTION = _("Arrays multiple labels onto a single sheet")
VERSION = "1.0.0"
AUTHOR = _("InvenTree contributors")
BLOCKING_PRINT = True
SETTINGS = {}
PrintingOptionsSerializer = LabelPrintingOptionsSerializer
def print_labels(self, label: LabelTemplate, items: list, request, **kwargs):
"""Handle printing of the provided labels"""
printing_options = kwargs['printing_options']
# Extract page size for the label sheet
page_size_code = printing_options.get('page_size', 'A4')
landscape = printing_options.get('landscape', False)
border = printing_options.get('border', False)
# Extract size of page
page_size = report.helpers.page_size(page_size_code)
page_width, page_height = page_size
if landscape:
page_width, page_height = page_height, page_width
# Calculate number of rows and columns
n_cols = math.floor(page_width / label.width)
n_rows = math.floor(page_height / label.height)
n_cells = n_cols * n_rows
if n_cells == 0:
raise ValidationError(_("Label is too large for page size"))
n_labels = len(items)
# Data to pass through to each page
document_data = {
"border": border,
"landscape": landscape,
"page_width": page_width,
"page_height": page_height,
"label_width": label.width,
"label_height": label.height,
"n_labels": n_labels,
"n_pages": math.ceil(n_labels / n_cells),
"n_cols": n_cols,
"n_rows": n_rows,
}
pages = []
idx = 0
while idx < n_labels:
if page := self.print_page(label, items[idx:idx + n_cells], request, **document_data):
pages.append(page)
idx += n_cells
if len(pages) == 0:
raise ValidationError(_("No labels were generated"))
# Render to a single HTML document
html_data = self.wrap_pages(pages, **document_data)
# Render HTML to PDF
html = weasyprint.HTML(string=html_data)
document = html.render().write_pdf()
output_file = ContentFile(document, 'labels.pdf')
output = LabelOutput.objects.create(
label=output_file,
user=request.user
)
return JsonResponse({
'file': output.label.url,
'success': True,
'message': f'{len(items)} labels generated'
})
def print_page(self, label: LabelTemplate, items: list, request, **kwargs):
"""Generate a single page of labels:
For a single page, generate a simple table grid of labels.
Styling of the table is handled by the higher level label template
Arguments:
label: The LabelTemplate object to use for printing
items: The list of database items to print (e.g. StockItem instances)
request: The HTTP request object which triggered this print job
Kwargs:
n_cols: Number of columns
n_rows: Number of rows
"""
n_cols = kwargs['n_cols']
n_rows = kwargs['n_rows']
# Generate a table of labels
html = """<table class='label-sheet-table'>"""
for row in range(n_rows):
html += "<tr class='label-sheet-row'>"
for col in range(n_cols):
html += f"<td class='label-sheet-cell label-sheet-row-{row} label-sheet-col-{col}'>"
# Cell index
idx = row * n_cols + col
if idx < len(items):
try:
# Render the individual label template
# Note that we disable @page styling for this
cell = label.render_as_string(
request,
target_object=items[idx],
insert_page_style=False
)
html += cell
except Exception as exc:
logger.exception("Error rendering label: %s", str(exc))
html += """
<div class='label-sheet-cell-error'></div>
"""
html += "</td>"
html += "</tr>"
html += "</table>"
return html
def wrap_pages(self, pages, **kwargs):
"""Wrap the generated pages into a single document"""
border = kwargs['border']
page_width = kwargs['page_width']
page_height = kwargs['page_height']
label_width = kwargs['label_width']
label_height = kwargs['label_height']
n_rows = kwargs['n_rows']
n_cols = kwargs['n_cols']
inner = ''.join(pages)
# Generate styles for individual cells (on each page)
cell_styles = []
for row in range(n_rows):
cell_styles.append(f"""
.label-sheet-row-{row} {{
top: {row * label_height}mm;
}}
""")
for col in range(n_cols):
cell_styles.append(f"""
.label-sheet-col-{col} {{
left: {col * label_width}mm;
}}
""")
cell_styles = "\n".join(cell_styles)
return f"""
<head>
<style>
@page {{
size: {page_width}mm {page_height}mm;
margin: 0mm;
padding: 0mm;
}}
.label-sheet-table {{
page-break-after: always;
table-layout: fixed;
width: {page_width}mm;
border-spacing: 0mm 0mm;
}}
.label-sheet-cell-error {{
background-color: #F00;
}}
.label-sheet-cell {{
border: {"1px solid #000;" if border else "0mm;"}
width: {label_width}mm;
height: {label_height}mm;
padding: 0mm;
position: absolute;
}}
{cell_styles}
body {{
margin: 0mm !important;
}}
</style>
</head>
<body>
{inner}
</body>
</html>
"""