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/
 | 
					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
 | 
				
			||||||
 | 
					        label = self.get_object()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # In debug mode, generate single HTML output, rather than PDF
 | 
					        # At this point, we offload the label(s) to the selected plugin.
 | 
				
			||||||
        debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE', cache=False)
 | 
					        # The plugin is responsible for handling the request and returning a response.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        label_name = "label.pdf"
 | 
					        result = plugin.print_labels(label, items_to_print, request)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        label_names = []
 | 
					 | 
				
			||||||
        label_instances = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Merge one or more PDF files into a single download
 | 
					 | 
				
			||||||
        for item in items_to_print:
 | 
					 | 
				
			||||||
            label = self.get_object()
 | 
					 | 
				
			||||||
            label.object_to_print = item
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            label_name = label.generate_filename(request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            label_names.append(label_name)
 | 
					 | 
				
			||||||
            label_instances.append(label)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if debug_mode and plugin is None:
 | 
					 | 
				
			||||||
                # Note that debug mode is only supported when not using a plugin
 | 
					 | 
				
			||||||
                outputs.append(label.render_as_string(request))
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                outputs.append(label.render(request))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not label_name.endswith(".pdf"):
 | 
					 | 
				
			||||||
            label_name += ".pdf"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if plugin is not None:
 | 
					 | 
				
			||||||
            """Label printing is to be handled by a plugin, rather than being exported to PDF.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            In this case, we do the following:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            - Individually generate each label, exporting as an image file
 | 
					 | 
				
			||||||
            - Pass all the images through to the label printing plugin
 | 
					 | 
				
			||||||
            - Return a JSON response indicating that the printing has been offloaded
 | 
					 | 
				
			||||||
            """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for idx, output in enumerate(outputs):
 | 
					 | 
				
			||||||
                """For each output, we generate a temporary image file, which will then get sent to the printer."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # Generate PDF data for the label
 | 
					 | 
				
			||||||
                pdf = output.get_document().write_pdf()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # Offload a background task to print the provided label
 | 
					 | 
				
			||||||
                offload_task(
 | 
					 | 
				
			||||||
                    plugin_label.print_label,
 | 
					 | 
				
			||||||
                    plugin.plugin_slug(),
 | 
					 | 
				
			||||||
                    pdf,
 | 
					 | 
				
			||||||
                    filename=label_names[idx],
 | 
					 | 
				
			||||||
                    label_instance=label_instances[idx],
 | 
					 | 
				
			||||||
                    user=request.user,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return JsonResponse({
 | 
					 | 
				
			||||||
                'plugin': plugin.plugin_slug(),
 | 
					 | 
				
			||||||
                'labels': label_names,
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        elif debug_mode:
 | 
					 | 
				
			||||||
            """Contatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            html = "\n".join(outputs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return HttpResponse(html)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if isinstance(result, JsonResponse):
 | 
				
			||||||
 | 
					            result['plugin'] = plugin.plugin_slug()
 | 
				
			||||||
 | 
					            return result
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            """Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
 | 
					            raise ValidationError(f"Plugin '{plugin.plugin_slug()}' returned invalid response type '{type(result)}'")
 | 
				
			||||||
 | 
					 | 
				
			||||||
            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,43 +32,30 @@ def print_label(plugin_slug: str, pdf_data, filename=None, label_instance=None,
 | 
				
			|||||||
        logger.error(f"Could not find matching plugin for '{plugin_slug}'")
 | 
					        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),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Log an error message to the database
 | 
					        user = kwargs.get('user', None)
 | 
				
			||||||
        log_error('plugin.print_label')
 | 
					 | 
				
			||||||
        logger.error(f"Label printing failed: Sending notification to user '{user}'")  # pragma: no cover
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Throw an error against the plugin instance
 | 
					        if user:
 | 
				
			||||||
        common.notifications.trigger_notification(
 | 
					            # Log an error message to the database
 | 
				
			||||||
            plugin.plugin_config(),
 | 
					            log_error('plugin.print_label')
 | 
				
			||||||
            'label.printing_failed',
 | 
					            logger.error(f"Label printing failed: Sending notification to user '{user}'")  # pragma: no cover
 | 
				
			||||||
            targets=[user],
 | 
					
 | 
				
			||||||
            context=ctx,
 | 
					            # Throw an error against the plugin instance
 | 
				
			||||||
            delivery_methods={common.notifications.UIMessageNotification, },
 | 
					            common.notifications.trigger_notification(
 | 
				
			||||||
        )
 | 
					                plugin.plugin_config(),
 | 
				
			||||||
 | 
					                'label.printing_failed',
 | 
				
			||||||
 | 
					                targets=[user],
 | 
				
			||||||
 | 
					                context=ctx,
 | 
				
			||||||
 | 
					                delivery_methods={common.notifications.UIMessageNotification, },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if settings.TESTING:
 | 
					        if settings.TESTING:
 | 
				
			||||||
            # If we are in testing mode, we want to know about this exception
 | 
					            # If we are in testing mode, we want to know about this exception
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user