mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
4d7fb751eb
commit
e8d16298a4
8
.gitignore
vendored
8
.gitignore
vendored
@ -40,9 +40,11 @@ inventree-demo-dataset/
|
|||||||
inventree-data/
|
inventree-data/
|
||||||
dummy_image.*
|
dummy_image.*
|
||||||
_tmp.csv
|
_tmp.csv
|
||||||
inventree/label.pdf
|
InvenTree/label.pdf
|
||||||
inventree/label.png
|
InvenTree/label.png
|
||||||
inventree/my_special*
|
label.pdf
|
||||||
|
label.png
|
||||||
|
InvenTree/my_special*
|
||||||
_tests*.txt
|
_tests*.txt
|
||||||
|
|
||||||
# Local static and media file storage (only when running in development mode)
|
# Local static and media file storage (only when running in development mode)
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# 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
|
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
|
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
|
- Changes 'serial_lte' and 'serial_gte' stock filters to point to 'serial_int' field
|
||||||
|
|
||||||
|
@ -1794,7 +1794,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
|
|
||||||
def label_printer_options():
|
def label_printer_options():
|
||||||
"""Build a list of available 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')
|
label_printer_plugins = registry.with_mixin('labels')
|
||||||
if label_printer_plugins:
|
if label_printer_plugins:
|
||||||
printers.extend([(p.slug, p.name + ' - ' + p.human_name) for p in label_printer_plugins])
|
printers.extend([(p.slug, p.name + ' - ' + p.human_name) for p in label_printer_plugins])
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
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.urls import include, path, re_path
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.cache import cache_page, never_cache
|
from django.views.decorators.cache import cache_page, never_cache
|
||||||
@ -18,9 +18,8 @@ import label.serializers
|
|||||||
from InvenTree.api import MetadataView
|
from InvenTree.api import MetadataView
|
||||||
from InvenTree.filters import InvenTreeSearchFilter
|
from InvenTree.filters import InvenTreeSearchFilter
|
||||||
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
|
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
|
||||||
from InvenTree.tasks import offload_task
|
|
||||||
from part.models import Part
|
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 plugin.registry import registry
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
|
||||||
@ -167,9 +166,10 @@ class LabelPrintMixin(LabelFilterMixin):
|
|||||||
|
|
||||||
plugin_key = request.query_params.get('plugin', None)
|
plugin_key = request.query_params.get('plugin', None)
|
||||||
|
|
||||||
# No plugin provided, and that's OK
|
# No plugin provided!
|
||||||
if plugin_key is None:
|
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)
|
plugin = registry.get_plugin(plugin_key)
|
||||||
|
|
||||||
@ -189,96 +189,21 @@ class LabelPrintMixin(LabelFilterMixin):
|
|||||||
|
|
||||||
if len(items_to_print) == 0:
|
if len(items_to_print) == 0:
|
||||||
# No valid items provided, return an error message
|
# No valid items provided, return an error message
|
||||||
|
|
||||||
raise ValidationError('No valid objects provided to label template')
|
raise ValidationError('No valid objects provided to label template')
|
||||||
|
|
||||||
outputs = []
|
# Label template
|
||||||
|
|
||||||
# In debug mode, generate single HTML output, rather than PDF
|
|
||||||
debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE', cache=False)
|
|
||||||
|
|
||||||
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 = self.get_object()
|
||||||
label.object_to_print = item
|
|
||||||
|
|
||||||
label_name = label.generate_filename(request)
|
# 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_names.append(label_name)
|
result = plugin.print_labels(label, items_to_print, request)
|
||||||
label_instances.append(label)
|
|
||||||
|
|
||||||
if debug_mode and plugin is None:
|
if isinstance(result, JsonResponse):
|
||||||
# Note that debug mode is only supported when not using a plugin
|
result['plugin'] = plugin.plugin_slug()
|
||||||
outputs.append(label.render_as_string(request))
|
return result
|
||||||
else:
|
else:
|
||||||
outputs.append(label.render(request))
|
raise ValidationError(f"Plugin '{plugin.plugin_slug()}' returned invalid response type '{type(result)}'")
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StockItemLabelMixin:
|
class StockItemLabelMixin:
|
||||||
|
26
InvenTree/label/migrations/0012_labeloutput.py
Normal file
26
InvenTree/label/migrations/0012_labeloutput.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -6,6 +6,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.core.validators import FileExtensionValidator, MinValueValidator
|
from django.core.validators import FileExtensionValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
@ -39,6 +40,13 @@ def rename_label(instance, filename):
|
|||||||
return os.path.join('label', 'template', instance.SUBDIR, 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):
|
def validate_stock_item_filters(filters):
|
||||||
"""Validate query filters for the StockItemLabel model"""
|
"""Validate query filters for the StockItemLabel model"""
|
||||||
filters = validateFilterString(filters, model=stock.models.StockItem)
|
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):
|
class StockItemLabel(LabelTemplate):
|
||||||
"""Template for printing StockItem labels."""
|
"""Template for printing StockItem labels."""
|
||||||
|
|
||||||
|
16
InvenTree/label/tasks.py
Normal file
16
InvenTree/label/tasks.py
Normal 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()
|
@ -1,17 +1,21 @@
|
|||||||
"""Tests for labels"""
|
"""Tests for labels"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
from django.http import JsonResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from InvenTree.helpers import validateFilterString
|
from InvenTree.helpers import validateFilterString
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
|
from label.models import LabelOutput
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
from plugin.registry import registry
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
from .models import PartLabel, StockItemLabel, StockLocationLabel
|
from .models import PartLabel, StockItemLabel, StockLocationLabel
|
||||||
@ -77,7 +81,16 @@ class LabelTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
for label in labels:
|
for label in labels:
|
||||||
url = reverse('api-part-label-print', kwargs={'pk': label.pk})
|
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):
|
def test_print_part_label(self):
|
||||||
"""Actually 'print' a label, and ensure that the correct information is contained."""
|
"""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)
|
# Ensure we are in "debug" mode (so the report is generated as HTML)
|
||||||
InvenTreeSetting.set_setting('REPORT_ENABLE', True, None)
|
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})
|
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
|
# Test that each element has been rendered correctly
|
||||||
self.assertIn("part: 1 - M2x4 LPHS", content)
|
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("http://testserver/part/1/", content)
|
||||||
self.assertIn("image: /static/img/blank_image.png", content)
|
self.assertIn("img/blank_image.png", content)
|
||||||
self.assertIn("logo: /static/img/inventree.png", content)
|
self.assertIn("img/inventree.png", content)
|
||||||
|
|
||||||
def test_metadata(self):
|
def test_metadata(self):
|
||||||
"""Unit tests for the metadata field."""
|
"""Unit tests for the metadata field."""
|
||||||
|
@ -5,30 +5,26 @@ import logging
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import pdf2image
|
|
||||||
|
|
||||||
import common.notifications
|
import common.notifications
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
from InvenTree.exceptions import log_error
|
from InvenTree.exceptions import log_error
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
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.
|
"""Print label with the provided plugin.
|
||||||
|
|
||||||
This task is nominally handled by the background worker.
|
This task is nominally handled by the background worker.
|
||||||
If the printing fails (throws an exception) then the user is notified.
|
If the printing fails (throws an exception) then the user is notified.
|
||||||
|
|
||||||
Args:
|
Arguments:
|
||||||
plugin_slug (str): The unique slug (key) of the plugin.
|
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.
|
kwargs:
|
||||||
label_instance (Union[LabelTemplate, None], optional): The template instance that should be printed. Defaults to None.
|
passed through to the plugin.print_label() method
|
||||||
user (Union[User, None], optional): User that should be informed of errors. Defaults to None.
|
|
||||||
"""
|
"""
|
||||||
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)
|
plugin = registry.get_plugin(plugin_slug)
|
||||||
|
|
||||||
@ -36,31 +32,18 @@ def print_label(plugin_slug: str, pdf_data, filename=None, label_instance=None,
|
|||||||
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
|
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
|
||||||
return
|
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:
|
try:
|
||||||
plugin.print_label(
|
plugin.print_label(**kwargs)
|
||||||
pdf_data=pdf_data,
|
|
||||||
png_file=png_file,
|
|
||||||
filename=filename,
|
|
||||||
label_instance=label_instance,
|
|
||||||
width=label_instance.width,
|
|
||||||
height=label_instance.height,
|
|
||||||
user=user
|
|
||||||
)
|
|
||||||
except Exception as e: # pragma: no cover
|
except Exception as e: # pragma: no cover
|
||||||
# Plugin threw an error - notify the user who attempted to print
|
# Plugin threw an error - notify the user who attempted to print
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'name': _('Label printing failed'),
|
'name': _('Label printing failed'),
|
||||||
'message': str(e),
|
'message': str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user = kwargs.get('user', None)
|
||||||
|
|
||||||
|
if user:
|
||||||
# Log an error message to the database
|
# Log an error message to the database
|
||||||
log_error('plugin.print_label')
|
log_error('plugin.print_label')
|
||||||
logger.error(f"Label printing failed: Sending notification to user '{user}'") # pragma: no cover
|
logger.error(f"Label printing failed: Sending notification to user '{user}'") # pragma: no cover
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
"""Plugin mixin classes for label plugins."""
|
"""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
|
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.
|
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:
|
class MixinMeta:
|
||||||
"""Meta options for this mixin."""
|
"""Meta options for this mixin."""
|
||||||
MIXIN_NAME = 'Label printing'
|
MIXIN_NAME = 'Label printing'
|
||||||
@ -20,17 +35,124 @@ class LabelPrintingMixin:
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.add_mixin('labels', True, __class__)
|
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):
|
def print_label(self, **kwargs):
|
||||||
"""Callback to print a single label.
|
"""Print a single label (blocking)
|
||||||
|
|
||||||
kwargs:
|
kwargs:
|
||||||
|
pdf_file: The PDF file object of the rendered label (WeasyTemplateResponse object)
|
||||||
pdf_data: Raw PDF data of the rendered label
|
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
|
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)
|
width: The expected width of the label (in mm)
|
||||||
height: The expected height 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)
|
# Unimplemented (to be implemented by the particular plugin class)
|
||||||
raise MixinNotImplementedError('This Plugin must implement a `print_label` method')
|
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
|
||||||
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Unit tests for the label printing mixin."""
|
"""Unit tests for the label printing mixin."""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
@ -7,7 +8,6 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
from label.models import PartLabel, StockItemLabel, StockLocationLabel
|
from label.models import PartLabel, StockItemLabel, StockLocationLabel
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
@ -77,11 +77,11 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
|||||||
"""Test that the sample printing plugin is installed."""
|
"""Test that the sample printing plugin is installed."""
|
||||||
# Get all label plugins
|
# Get all label plugins
|
||||||
plugins = registry.with_mixin('labels')
|
plugins = registry.with_mixin('labels')
|
||||||
self.assertEqual(len(plugins), 1)
|
self.assertEqual(len(plugins), 2)
|
||||||
|
|
||||||
# But, it is not 'active'
|
# But, it is not 'active'
|
||||||
plugins = registry.with_mixin('labels', active=True)
|
plugins = registry.with_mixin('labels', active=True)
|
||||||
self.assertEqual(len(plugins), 0)
|
self.assertEqual(len(plugins), 1)
|
||||||
|
|
||||||
def test_api(self):
|
def test_api(self):
|
||||||
"""Test that we can filter the API endpoint by mixin."""
|
"""Test that we can filter the API endpoint by mixin."""
|
||||||
@ -123,8 +123,8 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(len(response.data), 2)
|
||||||
data = response.data[0]
|
data = response.data[1]
|
||||||
self.assertEqual(data['key'], 'samplelabel')
|
self.assertEqual(data['key'], 'samplelabel')
|
||||||
|
|
||||||
def test_printing_process(self):
|
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)
|
self.get(self.do_url(Part.objects.all()[:2], None, label), expected_code=200)
|
||||||
|
|
||||||
# Print multiple parts without a plugin in debug mode
|
# 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)
|
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
|
# Print no part
|
||||||
self.get(self.do_url(None, plugin_ref, label), expected_code=400)
|
self.get(self.do_url(None, plugin_ref, label), expected_code=400)
|
||||||
|
0
InvenTree/plugin/builtin/labels/__init__.py
Normal file
0
InvenTree/plugin/builtin/labels/__init__.py
Normal file
96
InvenTree/plugin/builtin/labels/inventree_label.py
Normal file
96
InvenTree/plugin/builtin/labels/inventree_label.py
Normal 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)
|
@ -14,21 +14,22 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
|
|||||||
SLUG = "samplelabel"
|
SLUG = "samplelabel"
|
||||||
TITLE = "Sample Label Printer"
|
TITLE = "Sample Label Printer"
|
||||||
DESCRIPTION = "A sample plugin which provides a (fake) label printer interface"
|
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):
|
def print_label(self, **kwargs):
|
||||||
"""Sample printing step.
|
"""Sample printing step.
|
||||||
|
|
||||||
Normally here the connection to the printer and transfer of the label would take place.
|
Normally here the connection to the printer and transfer of the label would take place.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Test that the expected kwargs are present
|
# Test that the expected kwargs are present
|
||||||
print(f"Printing Label: {kwargs['filename']} (User: {kwargs['user']})")
|
print(f"Printing Label: {kwargs['filename']} (User: {kwargs['user']})")
|
||||||
print(f"Width: {kwargs['width']} x Height: {kwargs['height']}")
|
|
||||||
|
|
||||||
pdf_data = kwargs['pdf_data']
|
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
|
# Dump the PDF to a local file
|
||||||
with open(filename, 'wb') as pdf_out:
|
with open(filename, 'wb') as pdf_out:
|
||||||
|
@ -62,12 +62,12 @@ function inventreeGet(url, filters={}, options={}) {
|
|||||||
url: url,
|
url: url,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
data: filters,
|
data: filters,
|
||||||
dataType: 'json',
|
dataType: options.dataType || 'json',
|
||||||
contentType: 'application/json',
|
contentType: options.contentType || 'application/json',
|
||||||
async: (options.async == false) ? false : true,
|
async: (options.async == false) ? false : true,
|
||||||
success: function(response) {
|
success: function(response, status, xhr) {
|
||||||
if (options.success) {
|
if (options.success) {
|
||||||
options.success(response);
|
options.success(response, status, xhr);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(xhr, ajaxOptions, thrownError) {
|
error: function(xhr, ajaxOptions, thrownError) {
|
||||||
|
@ -59,7 +59,6 @@ function selectLabel(labels, items, options={}) {
|
|||||||
</label>
|
</label>
|
||||||
<div class='controls'>
|
<div class='controls'>
|
||||||
<select id='id_plugin' class='select form-control' name='plugin'>
|
<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) {
|
plugins.forEach(function(plugin) {
|
||||||
@ -207,19 +206,20 @@ function printLabels(options) {
|
|||||||
href += `${options.key}=${item}&`;
|
href += `${options.key}=${item}&`;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.plugin) {
|
|
||||||
href += `plugin=${data.plugin}`;
|
href += `plugin=${data.plugin}`;
|
||||||
|
|
||||||
inventreeGet(href, {}, {
|
inventreeGet(href, {}, {
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
|
if (response.file) {
|
||||||
|
// Download the generated file
|
||||||
|
window.open(response.file);
|
||||||
|
} else {
|
||||||
showMessage('{% trans "Labels sent to printer" %}', {
|
showMessage('{% trans "Labels sent to printer" %}', {
|
||||||
style: 'success',
|
style: 'success',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.open(href);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
plural_name: options.plural_name,
|
plural_name: options.plural_name,
|
||||||
singular_name: options.singular_name,
|
singular_name: options.singular_name,
|
||||||
|
@ -196,6 +196,7 @@ class RuleSet(models.Model):
|
|||||||
'common_projectcode',
|
'common_projectcode',
|
||||||
'common_webhookendpoint',
|
'common_webhookendpoint',
|
||||||
'common_webhookmessage',
|
'common_webhookmessage',
|
||||||
|
'label_labeloutput',
|
||||||
'users_owner',
|
'users_owner',
|
||||||
|
|
||||||
# Third-party tables
|
# Third-party tables
|
||||||
|
@ -4,9 +4,83 @@ title: Label Mixin
|
|||||||
|
|
||||||
## LabelPrintingMixin
|
## 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
|
### Web Integration
|
||||||
|
|
||||||
@ -22,7 +96,9 @@ Label printing plugins also allow direct printing of labels via the [mobile app]
|
|||||||
|
|
||||||
## Implementation
|
## 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
|
```python
|
||||||
from dummy_printer import printer_backend
|
from dummy_printer import printer_backend
|
||||||
@ -38,6 +114,9 @@ class MyLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
|
|||||||
SLUG = "mylabel"
|
SLUG = "mylabel"
|
||||||
TITLE = "A dummy printer"
|
TITLE = "A dummy printer"
|
||||||
|
|
||||||
|
# Set BLOCKING_PRINT to false to return immediately
|
||||||
|
BLOCKING_PRINT = False
|
||||||
|
|
||||||
def print_label(self, **kwargs):
|
def print_label(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Send the label to the printer
|
Send the label to the printer
|
||||||
@ -59,6 +138,12 @@ class MyLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
|
|||||||
printer_backend.print(png_file, w=width, h=height)
|
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
|
### 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.
|
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.
|
||||||
|
Loading…
Reference in New Issue
Block a user