mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +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:
		
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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])
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = []
 | 
			
		||||
 | 
			
		||||
        # 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 template
 | 
			
		||||
        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)
 | 
			
		||||
            label_instances.append(label)
 | 
			
		||||
        result = plugin.print_labels(label, items_to_print, request)
 | 
			
		||||
 | 
			
		||||
            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))
 | 
			
		||||
        if isinstance(result, JsonResponse):
 | 
			
		||||
            result['plugin'] = plugin.plugin_slug()
 | 
			
		||||
            return result
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
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
									
								
							
							
						
						
									
										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"""
 | 
			
		||||
 | 
			
		||||
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."""
 | 
			
		||||
 
 | 
			
		||||
@@ -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,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}'")
 | 
			
		||||
        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),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        user = kwargs.get('user', None)
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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"
 | 
			
		||||
    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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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}`;
 | 
			
		||||
 | 
			
		||||
                    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,
 | 
			
		||||
 
 | 
			
		||||
@@ -196,6 +196,7 @@ class RuleSet(models.Model):
 | 
			
		||||
        'common_projectcode',
 | 
			
		||||
        'common_webhookendpoint',
 | 
			
		||||
        'common_webhookmessage',
 | 
			
		||||
        'label_labeloutput',
 | 
			
		||||
        'users_owner',
 | 
			
		||||
 | 
			
		||||
        # Third-party tables
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user