2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-08-03 02:21:34 +00:00

Label plugin refactor ()

* Add skeleton for builtin label printing plugin

* Force selection of plugin when printing labels

* Enhance LabelPrintingMixin class

- Add render_to_pdf method
- Add render_to_html method

* Enhance plugin mixin

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

* Simplify print_label background function

- All arguments now handled by specific plugin

* Simplify label printing API

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

* Updated sample plugin

* Working on client side code

* Cleanup

* Update sample plugin

* Add new model type

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

* Update label printing plugin mixin

* Add background task to remove any old label outputs

* Open file if response contains filename

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

* Delete old labels after 5 days

* Remove debug statements

* Update API version

* Changed default behaviour to background printing

* Update label plugin mixin docs

* Provide default printer if none provided (legacy)

* Update unit test

* unit test updates

* Further fixes for unit tests

* unit test updates
This commit is contained in:
Oliver
2023-07-17 21:39:53 +10:00
committed by GitHub
parent 4d7fb751eb
commit e8d16298a4
18 changed files with 496 additions and 172 deletions
.gitignore
InvenTree
docs/docs/extend/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 = []
# Label template
label = self.get_object()
# In debug mode, generate single HTML output, rather than PDF
debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE', cache=False)
# At this point, we offload the label(s) to the selected plugin.
# The plugin is responsible for handling the request and returning a response.
label_name = "label.pdf"
label_names = []
label_instances = []
# Merge one or more PDF files into a single download
for item in items_to_print:
label = self.get_object()
label.object_to_print = item
label_name = label.generate_filename(request)
label_names.append(label_name)
label_instances.append(label)
if debug_mode and plugin is None:
# Note that debug mode is only supported when not using a plugin
outputs.append(label.render_as_string(request))
else:
outputs.append(label.render(request))
if not label_name.endswith(".pdf"):
label_name += ".pdf"
if plugin is not None:
"""Label printing is to be handled by a plugin, rather than being exported to PDF.
In this case, we do the following:
- Individually generate each label, exporting as an image file
- Pass all the images through to the label printing plugin
- Return a JSON response indicating that the printing has been offloaded
"""
for idx, output in enumerate(outputs):
"""For each output, we generate a temporary image file, which will then get sent to the printer."""
# Generate PDF data for the label
pdf = output.get_document().write_pdf()
# Offload a background task to print the provided label
offload_task(
plugin_label.print_label,
plugin.plugin_slug(),
pdf,
filename=label_names[idx],
label_instance=label_instances[idx],
user=request.user,
)
return JsonResponse({
'plugin': plugin.plugin_slug(),
'labels': label_names,
})
elif debug_mode:
"""Contatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
html = "\n".join(outputs)
return HttpResponse(html)
result = plugin.print_labels(label, items_to_print, request)
if isinstance(result, JsonResponse):
result['plugin'] = plugin.plugin_slug()
return result
else:
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
pages = []
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
inline = common.models.InvenTreeUserSetting.get_setting('LABEL_INLINE', user=request.user, cache=False)
return InvenTree.helpers.DownloadFile(
pdf,
label_name,
content_type='application/pdf',
inline=inline
)
raise ValidationError(f"Plugin '{plugin.plugin_slug()}' returned invalid response type '{type(result)}'")
class StockItemLabelMixin:

@@ -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

@@ -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."""