Label plugin refactor (#5251)

* Add skeleton for builtin label printing plugin

* Force selection of plugin when printing labels

* Enhance LabelPrintingMixin class

- Add render_to_pdf method
- Add render_to_html method

* Enhance plugin mixin

- Add class attribute to select blocking or non-blocking printing
- Add render_to_png method
- Add default method for printing multiple labels
- Add method for offloding print job

* Simplify print_label background function

- All arguments now handled by specific plugin

* Simplify label printing API

- Simply pass data to the particular plugin
- Check result type
- Return result

* Updated sample plugin

* Working on client side code

* Cleanup

* Update sample plugin

* Add new model type

- LabelOutput model
- Stores generated label file to the database
- Makes available for download

* Update label printing plugin mixin

* Add background task to remove any old label outputs

* Open file if response contains filename

* Remove "default printer" option which does not specify a plugin

* Delete old labels after 5 days

* Remove debug statements

* Update API version

* Changed default behaviour to background printing

* Update label plugin mixin docs

* Provide default printer if none provided (legacy)

* Update unit test

* unit test updates

* Further fixes for unit tests

* unit test updates
This commit is contained in:
Oliver 2023-07-17 21:39:53 +10:00 committed by GitHub
parent 4d7fb751eb
commit e8d16298a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 496 additions and 172 deletions

8
.gitignore vendored
View File

@ -40,9 +40,11 @@ inventree-demo-dataset/
inventree-data/
dummy_image.*
_tmp.csv
inventree/label.pdf
inventree/label.png
inventree/my_special*
InvenTree/label.pdf
InvenTree/label.png
label.pdf
label.png
InvenTree/my_special*
_tests*.txt
# Local static and media file storage (only when running in development mode)

View File

@ -2,11 +2,14 @@
# InvenTree API version
INVENTREE_API_VERSION = 129
INVENTREE_API_VERSION = 130
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v130 -> 2023-07-14 : https://github.com/inventree/InvenTree/pull/5251
- Refactor label printing interface
v129 -> 2023-07-06 : https://github.com/inventree/InvenTree/pull/5189
- Changes 'serial_lte' and 'serial_gte' stock filters to point to 'serial_int' field

View File

@ -1794,7 +1794,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
def label_printer_options():
"""Build a list of available label printer options."""
printers = [('', _('No Printer (Export to PDF)'))]
printers = []
label_printer_plugins = registry.with_mixin('labels')
if label_printer_plugins:
printers.extend([(p.slug, p.name + ' - ' + p.human_name) for p in label_printer_plugins])

View File

@ -2,7 +2,7 @@
from django.conf import settings
from django.core.exceptions import FieldError, ValidationError
from django.http import HttpResponse, JsonResponse
from django.http import JsonResponse
from django.urls import include, path, re_path
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page, never_cache
@ -18,9 +18,8 @@ import label.serializers
from InvenTree.api import MetadataView
from InvenTree.filters import InvenTreeSearchFilter
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
from InvenTree.tasks import offload_task
from part.models import Part
from plugin.base.label import label as plugin_label
from plugin.builtin.labels.inventree_label import InvenTreeLabelPlugin
from plugin.registry import registry
from stock.models import StockItem, StockLocation
@ -167,9 +166,10 @@ class LabelPrintMixin(LabelFilterMixin):
plugin_key = request.query_params.get('plugin', None)
# No plugin provided, and that's OK
# No plugin provided!
if plugin_key is None:
return None
# Default to the builtin label printing plugin
plugin_key = InvenTreeLabelPlugin.NAME.lower()
plugin = registry.get_plugin(plugin_key)
@ -189,96 +189,21 @@ class LabelPrintMixin(LabelFilterMixin):
if len(items_to_print) == 0:
# No valid items provided, return an error message
raise ValidationError('No valid objects provided to label template')
outputs = []
# Label template
label = self.get_object()
# In debug mode, generate single HTML output, rather than PDF
debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE', cache=False)
# At this point, we offload the label(s) to the selected plugin.
# The plugin is responsible for handling the request and returning a response.
label_name = "label.pdf"
label_names = []
label_instances = []
# 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
label_name = label.generate_filename(request)
label_names.append(label_name)
label_instances.append(label)
if debug_mode and plugin is None:
# Note that debug mode is only supported when not using a plugin
outputs.append(label.render_as_string(request))
else:
outputs.append(label.render(request))
if not label_name.endswith(".pdf"):
label_name += ".pdf"
if plugin is not None:
"""Label printing is to be handled by a plugin, rather than being exported to PDF.
In this case, we do the following:
- Individually generate each label, exporting as an image file
- Pass all the images through to the label printing plugin
- Return a JSON response indicating that the printing has been offloaded
"""
for idx, output in enumerate(outputs):
"""For each output, we generate a temporary image file, which will then get sent to the printer."""
# Generate PDF data for the label
pdf = output.get_document().write_pdf()
# Offload a background task to print the provided label
offload_task(
plugin_label.print_label,
plugin.plugin_slug(),
pdf,
filename=label_names[idx],
label_instance=label_instances[idx],
user=request.user,
)
return JsonResponse({
'plugin': plugin.plugin_slug(),
'labels': label_names,
})
elif 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)
result = plugin.print_labels(label, items_to_print, request)
if isinstance(result, JsonResponse):
result['plugin'] = plugin.plugin_slug()
return result
else:
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
pages = []
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()
inline = common.models.InvenTreeUserSetting.get_setting('LABEL_INLINE', user=request.user, cache=False)
return InvenTree.helpers.DownloadFile(
pdf,
label_name,
content_type='application/pdf',
inline=inline
)
raise ValidationError(f"Plugin '{plugin.plugin_slug()}' returned invalid response type '{type(result)}'")
class StockItemLabelMixin:

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.20 on 2023-07-14 11:55
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import label.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('label', '0011_auto_20230623_2158'),
]
operations = [
migrations.CreateModel(
name='LabelOutput',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.FileField(unique=True, upload_to=label.models.rename_label_output)),
('created', models.DateField(auto_now_add=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -6,6 +6,7 @@ import os
import sys
from django.conf import settings
from django.contrib.auth.models import User
from django.core.validators import FileExtensionValidator, MinValueValidator
from django.db import models
from django.template import Context, Template
@ -39,6 +40,13 @@ def rename_label(instance, filename):
return os.path.join('label', 'template', instance.SUBDIR, filename)
def rename_label_output(instance, filename):
"""Place the label output file into the correct subdirectory."""
filename = os.path.basename(filename)
return os.path.join('label', 'output', filename)
def validate_stock_item_filters(filters):
"""Validate query filters for the StockItemLabel model"""
filters = validateFilterString(filters, model=stock.models.StockItem)
@ -235,6 +243,36 @@ class LabelTemplate(MetadataMixin, models.Model):
)
class LabelOutput(models.Model):
"""Class representing a label output file
'Printing' a label may generate a file object (such as PDF)
which is made available for download.
Future work will offload this task to the background worker,
and provide a 'progress' bar for the user.
"""
# File will be stored in a subdirectory
label = models.FileField(
upload_to=rename_label_output,
unique=True, blank=False, null=False,
)
# Creation date of label output
created = models.DateField(
auto_now_add=True,
editable=False,
)
# User who generated the label
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
blank=True, null=True,
)
class StockItemLabel(LabelTemplate):
"""Template for printing StockItem labels."""

16
InvenTree/label/tasks.py Normal file
View File

@ -0,0 +1,16 @@
"""Background tasks for the label app"""
from datetime import timedelta
from django.utils import timezone
from InvenTree.tasks import ScheduledTask, scheduled_task
from label.models import LabelOutput
@scheduled_task(ScheduledTask.DAILY)
def cleanup_old_label_outputs():
"""Remove old label outputs from the database"""
# Remove any label outputs which are older than 30 days
LabelOutput.objects.filter(created__lte=timezone.now() - timedelta(days=5)).delete()

View File

@ -1,17 +1,21 @@
"""Tests for labels"""
import io
import json
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.http import JsonResponse
from django.urls import reverse
from common.models import InvenTreeSetting
from InvenTree.helpers import validateFilterString
from InvenTree.unit_test import InvenTreeAPITestCase
from label.models import LabelOutput
from part.models import Part
from plugin.registry import registry
from stock.models import StockItem
from .models import PartLabel, StockItemLabel, StockLocationLabel
@ -77,7 +81,16 @@ class LabelTest(InvenTreeAPITestCase):
for label in labels:
url = reverse('api-part-label-print', kwargs={'pk': label.pk})
self.get(f'{url}?parts={part.pk}', expected_code=200)
# Check that label printing returns the correct response type
response = self.get(f'{url}?parts={part.pk}', expected_code=200)
self.assertIsInstance(response, JsonResponse)
data = json.loads(response.content)
self.assertIn('message', data)
self.assertIn('file', data)
label_file = data['file']
self.assertIn('/media/label/output/', label_file)
def test_print_part_label(self):
"""Actually 'print' a label, and ensure that the correct information is contained."""
@ -115,21 +128,33 @@ class LabelTest(InvenTreeAPITestCase):
# Ensure we are in "debug" mode (so the report is generated as HTML)
InvenTreeSetting.set_setting('REPORT_ENABLE', True, None)
InvenTreeSetting.set_setting('REPORT_DEBUG_MODE', True, None)
# Print via the API
# Set the 'debug' setting for the plugin
plugin = registry.get_plugin('inventreelabel')
plugin.set_setting('DEBUG', True)
# Print via the API (Note: will default to the builtin plugin if no plugin supplied)
url = reverse('api-part-label-print', kwargs={'pk': label.pk})
response = self.get(f'{url}?parts=1', expected_code=200)
part_pk = Part.objects.first().pk
content = str(response.content)
response = self.get(f'{url}?parts={part_pk}', expected_code=200)
data = json.loads(response.content)
self.assertIn('file', data)
# Find the generated file
output = LabelOutput.objects.last()
# Open the file and read data
with open(output.label.path, 'r') as f:
content = f.read()
# Test that each element has been rendered correctly
self.assertIn("part: 1 - M2x4 LPHS", content)
self.assertIn('data: {"part": 1}', content)
self.assertIn(f'data: {{"part": {part_pk}}}', content)
self.assertIn("http://testserver/part/1/", content)
self.assertIn("image: /static/img/blank_image.png", content)
self.assertIn("logo: /static/img/inventree.png", content)
self.assertIn("img/blank_image.png", content)
self.assertIn("img/inventree.png", content)
def test_metadata(self):
"""Unit tests for the metadata field."""

View File

@ -5,30 +5,26 @@ import logging
from django.conf import settings
from django.utils.translation import gettext_lazy as _
import pdf2image
import common.notifications
from common.models import InvenTreeSetting
from InvenTree.exceptions import log_error
from plugin.registry import registry
logger = logging.getLogger('inventree')
def print_label(plugin_slug: str, pdf_data, filename=None, label_instance=None, user=None):
def print_label(plugin_slug: str, **kwargs):
"""Print label with the provided plugin.
This task is nominally handled by the background worker.
If the printing fails (throws an exception) then the user is notified.
Args:
Arguments:
plugin_slug (str): The unique slug (key) of the plugin.
pdf_data: Binary PDF data.
filename: The intended name of the printed label. Defaults to None.
label_instance (Union[LabelTemplate, None], optional): The template instance that should be printed. Defaults to None.
user (Union[User, None], optional): User that should be informed of errors. Defaults to None.
kwargs:
passed through to the plugin.print_label() method
"""
logger.info(f"Plugin '{plugin_slug}' is printing a label '{filename}'")
logger.info(f"Plugin '{plugin_slug}' is printing a label")
plugin = registry.get_plugin(plugin_slug)
@ -36,43 +32,30 @@ def print_label(plugin_slug: str, pdf_data, filename=None, label_instance=None,
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
return
# In addition to providing a .pdf image, we'll also provide a .png file
dpi = InvenTreeSetting.get_setting('LABEL_DPI', 300)
png_file = pdf2image.convert_from_bytes(
pdf_data,
dpi=dpi,
)[0]
try:
plugin.print_label(
pdf_data=pdf_data,
png_file=png_file,
filename=filename,
label_instance=label_instance,
width=label_instance.width,
height=label_instance.height,
user=user
)
plugin.print_label(**kwargs)
except Exception as e: # pragma: no cover
# Plugin threw an error - notify the user who attempted to print
ctx = {
'name': _('Label printing failed'),
'message': str(e),
}
# Log an error message to the database
log_error('plugin.print_label')
logger.error(f"Label printing failed: Sending notification to user '{user}'") # pragma: no cover
user = kwargs.get('user', None)
# Throw an error against the plugin instance
common.notifications.trigger_notification(
plugin.plugin_config(),
'label.printing_failed',
targets=[user],
context=ctx,
delivery_methods={common.notifications.UIMessageNotification, },
)
if user:
# Log an error message to the database
log_error('plugin.print_label')
logger.error(f"Label printing failed: Sending notification to user '{user}'") # pragma: no cover
# Throw an error against the plugin instance
common.notifications.trigger_notification(
plugin.plugin_config(),
'label.printing_failed',
targets=[user],
context=ctx,
delivery_methods={common.notifications.UIMessageNotification, },
)
if settings.TESTING:
# If we are in testing mode, we want to know about this exception

View File

@ -1,5 +1,13 @@
"""Plugin mixin classes for label plugins."""
from django.http import JsonResponse
import pdf2image
from common.models import InvenTreeSetting
from InvenTree.tasks import offload_task
from label.models import LabelTemplate
from plugin.base.label import label as plugin_label
from plugin.helpers import MixinNotImplementedError
@ -8,9 +16,16 @@ class LabelPrintingMixin:
Each plugin must provide a NAME attribute, which is used to uniquely identify the printer.
The plugin must also implement the print_label() function
The plugin *must* also implement the print_label() function for rendering an individual label
Note that the print_labels() function can also be overridden to provide custom behaviour.
"""
# If True, the print_label() method will block until the label is printed
# If False, the offload_label() method will be called instead
# By default, this is False, which means that labels will be printed in the background
BLOCKING_PRINT = False
class MixinMeta:
"""Meta options for this mixin."""
MIXIN_NAME = 'Label printing'
@ -20,17 +35,124 @@ class LabelPrintingMixin:
super().__init__()
self.add_mixin('labels', True, __class__)
def render_to_pdf(self, label: LabelTemplate, request, **kwargs):
"""Render this label to PDF format
Arguments:
label: The LabelTemplate object to render
request: The HTTP request object which triggered this print job
"""
return label.render(request)
def render_to_html(self, label: LabelTemplate, request, **kwargs):
"""Render this label to HTML format
Arguments:
label: The LabelTemplate object to render
request: The HTTP request object which triggered this print job
"""
return label.render_as_string(request)
def render_to_png(self, label: LabelTemplate, request=None, **kwargs):
"""Render this label to PNG format"""
# Check if pdf data is provided
pdf_data = kwargs.get('pdf_data', None)
if not pdf_data:
pdf_data = self.render_to_pdf(label, request, **kwargs).get_document().write_pdf()
dpi = kwargs.get(
'dpi',
InvenTreeSetting.get_setting('LABEL_DPI', 300)
)
# Convert to png data
png = pdf2image.convert_from_bytes(pdf_data, dpi=dpi)[0]
return png
def print_labels(self, label: LabelTemplate, items: list, request, **kwargs):
"""Print one or more labels with the provided template and items.
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
Returns:
A JSONResponse object which indicates outcome to the user
The default implementation simply calls print_label() for each label, producing multiple single label output "jobs"
but this can be overridden by the particular plugin.
"""
try:
user = request.user
except AttributeError:
user = None
# Generate a label output for each provided item
for item in items:
label.object_to_print = item
filename = label.generate_filename(request)
pdf_file = self.render_to_pdf(label, request, **kwargs)
pdf_data = pdf_file.get_document().write_pdf()
png_file = self.render_to_png(label, request, pdf_data=pdf_data, **kwargs)
print_args = {
'pdf_file': pdf_file,
'pdf_data': pdf_data,
'png_file': png_file,
'filename': filename,
'label_instance': label,
'item_instance': item,
'user': user,
'width': label.width,
'height': label.height,
}
if self.BLOCKING_PRINT:
# Blocking print job
self.print_label(**print_args)
else:
# Non-blocking print job
self.offload_label(**print_args)
# Return a JSON response to the user
return JsonResponse({
'success': True,
'message': f'{len(items)} labels printed',
})
def print_label(self, **kwargs):
"""Callback to print a single label.
"""Print a single label (blocking)
kwargs:
pdf_file: The PDF file object of the rendered label (WeasyTemplateResponse object)
pdf_data: Raw PDF data of the rendered label
png_file: An in-memory PIL image file, rendered at 300dpi
filename: The filename of this PDF label
label_instance: The instance of the label model which triggered the print_label() method
item_instance: The instance of the database model against which the label is printed
user: The user who triggered this print job
width: The expected width of the label (in mm)
height: The expected height of the label (in mm)
filename: The filename of this PDF label
user: The user who printed this label
Note that the supplied kwargs may be different if the plugin overrides the print_labels() method.
"""
# Unimplemented (to be implemented by the particular plugin class)
raise MixinNotImplementedError('This Plugin must implement a `print_label` method')
def offload_label(self, **kwargs):
"""Offload a single label (non-blocking)
Instead of immediately printing the label (which is a blocking process),
this method should offload the label to a background worker process.
Offloads a call to the 'print_label' method (of this plugin) to a background worker.
"""
offload_task(
plugin_label.print_label,
self.plugin_slug(),
**kwargs
)

View File

@ -1,5 +1,6 @@
"""Unit tests for the label printing mixin."""
import json
import os
from django.apps import apps
@ -7,7 +8,6 @@ from django.urls import reverse
from PIL import Image
from common.models import InvenTreeSetting
from InvenTree.unit_test import InvenTreeAPITestCase
from label.models import PartLabel, StockItemLabel, StockLocationLabel
from part.models import Part
@ -77,11 +77,11 @@ class LabelMixinTests(InvenTreeAPITestCase):
"""Test that the sample printing plugin is installed."""
# Get all label plugins
plugins = registry.with_mixin('labels')
self.assertEqual(len(plugins), 1)
self.assertEqual(len(plugins), 2)
# But, it is not 'active'
plugins = registry.with_mixin('labels', active=True)
self.assertEqual(len(plugins), 0)
self.assertEqual(len(plugins), 1)
def test_api(self):
"""Test that we can filter the API endpoint by mixin."""
@ -123,8 +123,8 @@ class LabelMixinTests(InvenTreeAPITestCase):
}
)
self.assertEqual(len(response.data), 1)
data = response.data[0]
self.assertEqual(len(response.data), 2)
data = response.data[1]
self.assertEqual(data['key'], 'samplelabel')
def test_printing_process(self):
@ -160,9 +160,10 @@ class LabelMixinTests(InvenTreeAPITestCase):
self.get(self.do_url(Part.objects.all()[:2], None, label), expected_code=200)
# Print multiple parts without a plugin in debug mode
InvenTreeSetting.set_setting('REPORT_DEBUG_MODE', True, None)
response = self.get(self.do_url(Part.objects.all()[:2], None, label), expected_code=200)
self.assertIn('@page', str(response.content))
data = json.loads(response.content)
self.assertIn('file', data)
# Print no part
self.get(self.do_url(None, plugin_ref, label), expected_code=400)

View File

@ -0,0 +1,96 @@
"""Default label printing plugin (supports PDF generation)"""
from django.core.files.base import ContentFile
from django.http import JsonResponse
from django.utils.translation import gettext_lazy as _
from label.models import LabelOutput, LabelTemplate
from plugin import InvenTreePlugin
from plugin.mixins import LabelPrintingMixin, SettingsMixin
class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
"""Builtin plugin for label printing.
This plugin merges the selected labels into a single PDF file,
which is made available for download.
"""
NAME = "InvenTreeLabel"
TITLE = _("InvenTree PDF label printer")
DESCRIPTION = _("Provides native support for printing PDF labels")
VERSION = "1.0.0"
AUTHOR = _("InvenTree contributors")
BLOCKING_PRINT = True
SETTINGS = {
'DEBUG': {
'name': _('Debug mode'),
'description': _('Enable debug mode - returns raw HTML instead of PDF'),
'validator': bool,
'default': False,
},
}
def print_labels(self, label: LabelTemplate, items: list, request, **kwargs):
"""Handle printing of multiple labels
- Label outputs are concatenated together, and we return a single PDF file.
- If DEBUG mode is enabled, we return a single HTML file.
"""
debug = self.get_setting('DEBUG')
outputs = []
output_file = None
for item in items:
label.object_to_print = item
outputs.append(self.print_label(label, request, debug=debug, **kwargs))
if self.get_setting('DEBUG'):
html = '\n'.join(outputs)
output_file = ContentFile(html, 'labels.html')
else:
pages = []
# Following process is required to stitch labels together into a single PDF
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()
# Create label output file
output_file = ContentFile(pdf, 'labels.pdf')
# Save the generated file to the database
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_label(self, label: LabelTemplate, request, **kwargs):
"""Handle printing of a single label.
Returns either a PDF or HTML output, depending on the DEBUG setting.
"""
debug = kwargs.get('debug', self.get_setting('DEBUG'))
if debug:
return self.render_to_html(label, request, **kwargs)
else:
return self.render_to_pdf(label, request, **kwargs)

View File

@ -14,21 +14,22 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
SLUG = "samplelabel"
TITLE = "Sample Label Printer"
DESCRIPTION = "A sample plugin which provides a (fake) label printer interface"
VERSION = "0.2"
AUTHOR = "InvenTree contributors"
VERSION = "0.3.0"
def print_label(self, **kwargs):
"""Sample printing step.
Normally here the connection to the printer and transfer of the label would take place.
"""
# Test that the expected kwargs are present
print(f"Printing Label: {kwargs['filename']} (User: {kwargs['user']})")
print(f"Width: {kwargs['width']} x Height: {kwargs['height']}")
pdf_data = kwargs['pdf_data']
png_file = kwargs['png_file']
png_file = self.render_to_png(label=None, pdf_data=pdf_data)
filename = kwargs['filename']
filename = 'label.pdf'
# Dump the PDF to a local file
with open(filename, 'wb') as pdf_out:

View File

@ -62,12 +62,12 @@ function inventreeGet(url, filters={}, options={}) {
url: url,
type: 'GET',
data: filters,
dataType: 'json',
contentType: 'application/json',
dataType: options.dataType || 'json',
contentType: options.contentType || 'application/json',
async: (options.async == false) ? false : true,
success: function(response) {
success: function(response, status, xhr) {
if (options.success) {
options.success(response);
options.success(response, status, xhr);
}
},
error: function(xhr, ajaxOptions, thrownError) {

View File

@ -59,7 +59,6 @@ function selectLabel(labels, items, options={}) {
</label>
<div class='controls'>
<select id='id_plugin' class='select form-control' name='plugin'>
<option value='' title='{% trans "Export to PDF" %}'>{% trans "Export to PDF" %}</option>
`;
plugins.forEach(function(plugin) {
@ -207,19 +206,20 @@ function printLabels(options) {
href += `${options.key}=${item}&`;
});
if (data.plugin) {
href += `plugin=${data.plugin}`;
href += `plugin=${data.plugin}`;
inventreeGet(href, {}, {
success: function(response) {
inventreeGet(href, {}, {
success: function(response) {
if (response.file) {
// Download the generated file
window.open(response.file);
} else {
showMessage('{% trans "Labels sent to printer" %}', {
style: 'success',
});
}
});
} else {
window.open(href);
}
}
});
},
plural_name: options.plural_name,
singular_name: options.singular_name,

View File

@ -196,6 +196,7 @@ class RuleSet(models.Model):
'common_projectcode',
'common_webhookendpoint',
'common_webhookmessage',
'label_labeloutput',
'users_owner',
# Third-party tables

View File

@ -4,9 +4,83 @@ title: Label Mixin
## LabelPrintingMixin
The `LabelPrintingMixin` class enables plugins to print labels directly to a connected printer. Custom plugins can be written to support any printer backend.
The `LabelPrintingMixin` class allows plugins to provide custom label printing functionality. The specific implementation of a label printing plugin is quite flexible, allowing for the following functions (as a starting point):
An example of this is the [inventree-brother-plugin](https://github.com/inventree/inventree-brother-plugin) which provides native support for the Brother QL and PT series of networked label printers.
- Printing a single label to a file, and allowing user to download
- Combining multiple labels onto a single page
- Supporting proprietary label sheet formats
- Offloading label printing to an external printer
### Entry Point
When printing labels against a particular plugin, the entry point is the `print_labels` method. The default implementation of this method iterates over each of the provided items, renders a PDF, and calls the `print_label` method for each item, providing the rendered PDF data.
Both the `print_labels` and `print_label` methods may be overridden by a plugin, allowing for complex functionality to be achieved.
For example, the `print_labels` method could be reimplemented to merge all labels into a single larger page, and return a single page for printing.
### Return Type
The `print_labels` method *must* return a JsonResponse object. If the method does not return such a response, an error will be raised by the server.
### File Generation
If the label printing plugin generates a real file, it should be stored as a `LabelOutput` instance in the database, and returned in the JsonResponse result under the 'file' key.
For example, the built-in `InvenTreeLabelPlugin` plugin generates a PDF file which contains all the provided labels concatenated together. A snippet of the code is shown below (refer to the source code for full details):
```python
# Save the generated file to the database
output = LabelOutput.objects.create(
label=output_file,
user=request.user
)
return JsonResponse({
'file': output.label.url,
'success': True,
'message': f'{len(items)} labels generated'
})
```
### Background Printing
For some label printing processes (such as offloading printing to an external networked printer) it may be preferable to utilize the background worker process, and not block the front-end server.
The plugin provides an easy method to offload printing to the background thread.
Simply override the class attribute `BLOCKING_PRINT` as follows:
```python
class MyPrinterPlugin(LabelPrintingMixin, InvenTreePlugin):
BLOCKING_PRINT = False
```
If the `print_labels` method is not changed, this will run the `print_label` method in a background worker thread.
!!! info "Example Plugin"
Check out the [inventree-brother-plugin](https://github.com/inventree/inventree-brother-plugin) which provides native support for the Brother QL and PT series of networked label printers
!!! tip "Custom Code"
If your plugin overrides the `print_labels` method, you will have to ensure that the label printing is correctly offloaded to the background worker. Look at the `offload_label` method of the plugin mixin class for how this can be achieved.
### Helper Methods
The plugin class provides a number of additional helper methods which may be useful for generating labels:
| Method | Description |
| --- | --- |
| render_to_pdf | Render label template to an in-memory PDF object |
| render_to_html | Render label template to a raw HTML string |
| render_to_png | Convert PDF data to an in-memory PNG image |
!!! info "Use the Source"
These methods are available for more complex implementations - refer to the source code for more information!
### Merging Labels
To merge (combine) multiple labels into a single output (for example printing multiple labels on a single sheet of paper), the plugin must override the `print_labels` method and implement the required functionality.
## Integration
### Web Integration
@ -22,7 +96,9 @@ Label printing plugins also allow direct printing of labels via the [mobile app]
## Implementation
Plugins which implement the `LabelPrintingMixin` mixin class must provide a `print_label` function:
Plugins which implement the `LabelPrintingMixin` mixin class can be implemented by simply providing a `print_label` method.
### Simple Example
```python
from dummy_printer import printer_backend
@ -38,6 +114,9 @@ class MyLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
SLUG = "mylabel"
TITLE = "A dummy printer"
# Set BLOCKING_PRINT to false to return immediately
BLOCKING_PRINT = False
def print_label(self, **kwargs):
"""
Send the label to the printer
@ -59,6 +138,12 @@ class MyLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
printer_backend.print(png_file, w=width, h=height)
```
### Default Plugin
InvenTree supplies the `InvenTreeLabelPlugin` out of the box, which generates a PDF file which is then available for immediate download by the user.
The default plugin also features a *DEBUG* mode which generates a raw HTML output, rather than PDF. This can be handy for tracking down any template rendering errors in your labels.
### Available Data
The *label* data are supplied to the plugin in both `PDF` and `PNG` formats. This provides compatibility with a great range of label printers "out of the box". Conversion to other formats, if required, is left as an exercise for the plugin developer.