Printing options (#5786)

* Added backend changes to support printing options

* Pass printing options seperatly via kwargs for easier api refactor later

* Implemented printing options in CUI

* Fix js linting

* Use translations for printing dialog

* Added docs

* Remove plugin and template fields from send printing options

* Fix docs

* Added tests

* Fix tests

* Fix options response and added test for it

* Fix tests

* Bump api version

* Update docs

* Apply suggestions from code review

* Fix api change date
This commit is contained in:
Lukas 2023-11-01 14:39:19 +01:00 committed by GitHub
parent 59f17a9885
commit a11418398f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 303 additions and 180 deletions

View File

@ -2,11 +2,14 @@
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 144 INVENTREE_API_VERSION = 145
""" """
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
v145 -> 2023-10-30: https://github.com/inventree/InvenTree/pull/5786
- Allow printing labels via POST including printing options in the body
v144 -> 2023-10-23: https://github.com/inventree/InvenTree/pull/5811 v144 -> 2023-10-23: https://github.com/inventree/InvenTree/pull/5811
- Adds version information API endpoint - Adds version information API endpoint
@ -106,7 +109,6 @@ v115 - > 2023-05-18 : https://github.com/inventree/InvenTree/pull/4846
v114 -> 2023-05-16 : https://github.com/inventree/InvenTree/pull/4825 v114 -> 2023-05-16 : https://github.com/inventree/InvenTree/pull/4825
- Adds "delivery_date" to shipments - Adds "delivery_date" to shipments
>>>>>>> inventree/master
v113 -> 2023-05-13 : https://github.com/inventree/InvenTree/pull/4800 v113 -> 2023-05-13 : https://github.com/inventree/InvenTree/pull/4800
- Adds API endpoints for scrapping a build output - Adds API endpoints for scrapping a build output

View File

@ -84,6 +84,10 @@ class InvenTreeMetadata(SimpleMetadata):
'DELETE': 'delete', 'DELETE': 'delete',
} }
# let the view define a custom rolemap
if hasattr(view, "rolemap"):
rolemap.update(view.rolemap)
# Remove any HTTP methods that the user does not have permission for # Remove any HTTP methods that the user does not have permission for
for method, permission in rolemap.items(): for method, permission in rolemap.items():

View File

@ -61,6 +61,10 @@ class RolePermission(permissions.BasePermission):
'DELETE': 'delete', 'DELETE': 'delete',
} }
# let the view define a custom rolemap
if hasattr(view, "rolemap"):
rolemap.update(view.rolemap)
permission = rolemap[request.method] permission = rolemap[request.method]
# The required role may be defined for the view class # The required role may be defined for the view class

View File

@ -7,7 +7,9 @@ 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
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import serializers
from rest_framework.exceptions import NotFound from rest_framework.exceptions import NotFound
from rest_framework.request import clone_request
import build.models import build.models
import common.models import common.models
@ -136,17 +138,45 @@ class LabelListView(LabelFilterMixin, ListAPI):
class LabelPrintMixin(LabelFilterMixin): class LabelPrintMixin(LabelFilterMixin):
"""Mixin for printing labels.""" """Mixin for printing labels."""
rolemap = {
"GET": "view",
"POST": "view",
}
def check_permissions(self, request):
"""Override request method to GET so that also non superusers can print using a post request."""
if request.method == "POST":
request = clone_request(request, "GET")
return super().check_permissions(request)
@method_decorator(never_cache) @method_decorator(never_cache)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
"""Prevent caching when printing report templates""" """Prevent caching when printing report templates"""
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
def get_serializer(self, *args, **kwargs):
"""Define a get_serializer method to be discoverable by the OPTIONS request."""
# Check the request to determine if the user has selected a label printing plugin
plugin = self.get_plugin(self.request)
serializer = plugin.get_printing_options_serializer(self.request)
# if no serializer is defined, return an empty serializer
if not serializer:
return serializers.Serializer()
return serializer
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Perform a GET request against this endpoint to print labels""" """Perform a GET request against this endpoint to print labels"""
common.models.InvenTreeUserSetting.set_setting('DEFAULT_' + self.ITEM_KEY.upper() + '_LABEL_TEMPLATE', common.models.InvenTreeUserSetting.set_setting('DEFAULT_' + self.ITEM_KEY.upper() + '_LABEL_TEMPLATE',
self.get_object().pk, None, user=request.user) self.get_object().pk, None, user=request.user)
return self.print(request, self.get_items()) return self.print(request, self.get_items())
def post(self, request, *args, **kwargs):
"""Perform a GET request against this endpoint to print labels"""
return self.get(request, *args, **kwargs)
def get_plugin(self, request): def get_plugin(self, request):
"""Return the label printing plugin associated with this request. """Return the label printing plugin associated with this request.
@ -167,14 +197,18 @@ class LabelPrintMixin(LabelFilterMixin):
plugin = registry.get_plugin(plugin_key) plugin = registry.get_plugin(plugin_key)
if plugin: if not plugin:
if plugin.is_active():
# Only return the plugin if it is enabled!
return plugin
raise ValidationError(f"Plugin '{plugin_key}' is not enabled")
else:
raise NotFound(f"Plugin '{plugin_key}' not found") raise NotFound(f"Plugin '{plugin_key}' not found")
if not plugin.is_active():
raise ValidationError(f"Plugin '{plugin_key}' is not enabled")
if not plugin.mixin_enabled("labels"):
raise ValidationError(f"Plugin '{plugin_key}' is not a label printing plugin")
# Only return the plugin if it is enabled and has the label printing mixin
return plugin
def print(self, request, items_to_print): def print(self, request, items_to_print):
"""Print this label template against a number of pre-validated items.""" """Print this label template against a number of pre-validated items."""
# Check the request to determine if the user has selected a label printing plugin # Check the request to determine if the user has selected a label printing plugin
@ -187,10 +221,14 @@ class LabelPrintMixin(LabelFilterMixin):
# Label template # Label template
label = self.get_object() label = self.get_object()
# if the plugin returns a serializer, validate the data
if serializer := plugin.get_printing_options_serializer(request, data=request.data):
serializer.is_valid(raise_exception=True)
# At this point, we offload the label(s) to the selected plugin. # At this point, we offload the label(s) to the selected plugin.
# The plugin is responsible for handling the request and returning a response. # The plugin is responsible for handling the request and returning a response.
result = plugin.print_labels(label, items_to_print, request) result = plugin.print_labels(label, items_to_print, request, printing_options=request.data)
if isinstance(result, JsonResponse): if isinstance(result, JsonResponse):
result['plugin'] = plugin.plugin_slug() result['plugin'] = plugin.plugin_slug()

View File

@ -1,8 +1,12 @@
"""Plugin mixin classes for label plugins.""" """Plugin mixin classes for label plugins."""
from typing import Union
from django.http import JsonResponse from django.http import JsonResponse
import pdf2image import pdf2image
from rest_framework import serializers
from rest_framework.request import Request
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from InvenTree.tasks import offload_task from InvenTree.tasks import offload_task
@ -18,7 +22,7 @@ class LabelPrintingMixin:
The plugin *must* also implement the print_label() function for rendering an individual label 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. Note that the print_labels() function can also be overridden to provide custom behavior.
""" """
# If True, the print_label() method will block until the label is printed # If True, the print_label() method will block until the label is printed
@ -70,7 +74,7 @@ class LabelPrintingMixin:
png = pdf2image.convert_from_bytes(pdf_data, dpi=dpi)[0] png = pdf2image.convert_from_bytes(pdf_data, dpi=dpi)[0]
return png return png
def print_labels(self, label: LabelTemplate, items: list, request, **kwargs): def print_labels(self, label: LabelTemplate, items: list, request: Request, printing_options: dict, **kwargs):
"""Print one or more labels with the provided template and items. """Print one or more labels with the provided template and items.
Arguments: Arguments:
@ -78,6 +82,9 @@ class LabelPrintingMixin:
items: The list of database items to print (e.g. StockItem instances) items: The list of database items to print (e.g. StockItem instances)
request: The HTTP request object which triggered this print job request: The HTTP request object which triggered this print job
Keyword arguments:
printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
Returns: Returns:
A JSONResponse object which indicates outcome to the user A JSONResponse object which indicates outcome to the user
@ -107,6 +114,7 @@ class LabelPrintingMixin:
'user': user, 'user': user,
'width': label.width, 'width': label.width,
'height': label.height, 'height': label.height,
'printing_options': printing_options,
} }
if self.BLOCKING_PRINT: if self.BLOCKING_PRINT:
@ -136,6 +144,7 @@ class LabelPrintingMixin:
user: The user who triggered this print job 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)
printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
Note that the supplied kwargs may be different if the plugin overrides the print_labels() method. Note that the supplied kwargs may be different if the plugin overrides the print_labels() method.
""" """
@ -158,3 +167,21 @@ class LabelPrintingMixin:
self.plugin_slug(), self.plugin_slug(),
**kwargs **kwargs
) )
def get_printing_options_serializer(self, request: Request, *args, **kwargs) -> Union[serializers.Serializer, None]:
"""Return a serializer class instance with dynamic printing options.
Arguments:
request: The request made to print a label or interfering the available serializer fields via an OPTIONS request
*args, **kwargs: need to be passed to the serializer instance
Returns:
A class instance of a DRF serializer class, by default this an instance of
self.PrintingOptionsSerializer using the *args, **kwargs if existing for this plugin
"""
serializer = getattr(self, "PrintingOptionsSerializer", None)
if not serializer:
return None
return serializer(*args, **kwargs)

View File

@ -2,6 +2,7 @@
import json import json
import os import os
from unittest import mock
from django.apps import apps from django.apps import apps
from django.urls import reverse from django.urls import reverse
@ -183,6 +184,32 @@ class LabelMixinTests(InvenTreeAPITestCase):
# And that it is a valid image file # And that it is a valid image file
Image.open('label.png') Image.open('label.png')
def test_printing_options(self):
"""Test printing options."""
# Ensure the labels were created
apps.get_app_config('label').create_labels()
# Lookup references
plugin_ref = 'samplelabelprinter'
label = PartLabel.objects.first()
self.do_activate_plugin()
# test options response
options = self.options(self.do_url(Part.objects.all()[:2], plugin_ref, label), expected_code=200).json()
self.assertTrue("amount" in options["actions"]["POST"])
plg = registry.get_plugin(plugin_ref)
with mock.patch.object(plg, "print_label") as print_label:
# wrong value type
res = self.post(self.do_url(Part.objects.all()[:2], plugin_ref, label), data={"amount": "-no-valid-int-"}, expected_code=400).json()
self.assertTrue("amount" in res)
print_label.assert_not_called()
# correct value type
self.post(self.do_url(Part.objects.all()[:2], plugin_ref, label), data={"amount": 13}, expected_code=200).json()
self.assertEqual(print_label.call_args.kwargs["printing_options"], {"amount": 13})
def test_printing_endpoints(self): def test_printing_endpoints(self):
"""Cover the endpoints not covered by `test_printing_process`.""" """Cover the endpoints not covered by `test_printing_process`."""
plugin_ref = 'samplelabelprinter' plugin_ref = 'samplelabelprinter'

View File

@ -3,6 +3,8 @@
This does not function in real usage and is more to show the required components and for unit tests. This does not function in real usage and is more to show the required components and for unit tests.
""" """
from rest_framework import serializers
from plugin import InvenTreePlugin from plugin import InvenTreePlugin
from plugin.mixins import LabelPrintingMixin from plugin.mixins import LabelPrintingMixin
@ -17,6 +19,10 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
AUTHOR = "InvenTree contributors" AUTHOR = "InvenTree contributors"
VERSION = "0.3.0" VERSION = "0.3.0"
class PrintingOptionsSerializer(serializers.Serializer):
"""Serializer to return printing options."""
amount = serializers.IntegerField(required=False, default=1)
def print_label(self, **kwargs): def print_label(self, **kwargs):
"""Sample printing step. """Sample printing step.

View File

@ -32,6 +32,7 @@
setFormGroupVisibility, setFormGroupVisibility,
showFormInput, showFormInput,
selectImportFields, selectImportFields,
updateForm,
*/ */
/** /**
@ -306,6 +307,7 @@ function constructDeleteForm(fields, options) {
* - hidden: Set to true to hide the field * - hidden: Set to true to hide the field
* - icon: font-awesome icon to display before the field * - icon: font-awesome icon to display before the field
* - prefix: Custom HTML prefix to display before the field * - prefix: Custom HTML prefix to display before the field
* - localOnly: If true, this field will only be rendered, but not send to the server
* - data: map of data to fill out field values with * - data: map of data to fill out field values with
* - focus: Name of field to focus on when modal is displayed * - focus: Name of field to focus on when modal is displayed
* - preventClose: Set to true to prevent form from closing on success * - preventClose: Set to true to prevent form from closing on success
@ -315,6 +317,7 @@ function constructDeleteForm(fields, options) {
* - reload: Set to true to reload the current page after form success * - reload: Set to true to reload the current page after form success
* - confirm: Set to true to require a "confirm" button * - confirm: Set to true to require a "confirm" button
* - confirmText: Text for confirm button (default = "Confirm") * - confirmText: Text for confirm button (default = "Confirm")
* - disableSuccessMessage: Set to true to suppress the success message if the response contains a success key by accident
* *
*/ */
function constructForm(url, options={}) { function constructForm(url, options={}) {
@ -720,6 +723,21 @@ function constructFormBody(fields, options) {
}); });
} }
/**
* This Method updates an existing form by replacing all form fields with the new ones
* @param {*} options new form definition options
*/
function updateForm(options) {
// merge already entered values in the newly constructed form
options.data = extractFormData(options.fields, options);
// remove old submit handlers
$(options.modal).off('click', '#modal-form-submit');
// construct new form
constructFormBody(options.fields, options);
}
// Add a "confirm" checkbox to the modal // Add a "confirm" checkbox to the modal
// The "submit" button will be disabled unless "confirm" is checked // The "submit" button will be disabled unless "confirm" is checked
@ -841,6 +859,7 @@ function submitFormData(fields, options) {
// Ignore visual fields // Ignore visual fields
if (field && field.type == 'candy') continue; if (field && field.type == 'candy') continue;
if (field && field.localOnly === true) continue;
if (field) { if (field) {
@ -1190,7 +1209,7 @@ function handleFormSuccess(response, options) {
} }
// Display any messages // Display any messages
if (response && (response.success || options.successMessage)) { if (!options.disableSuccessMessage && response && (response.success || options.successMessage)) {
showAlertOrCache( showAlertOrCache(
response.success || options.successMessage, response.success || options.successMessage,
cache, cache,

View File

@ -4,6 +4,8 @@
/* globals /* globals
attachSelect, attachSelect,
closeModal, closeModal,
constructForm,
getFormFieldValue,
inventreeGet, inventreeGet,
makeOptionsList, makeOptionsList,
modalEnable, modalEnable,
@ -13,7 +15,9 @@
modalSubmit, modalSubmit,
openModal, openModal,
showAlertDialog, showAlertDialog,
showApiError,
showMessage, showMessage,
updateForm,
user_settings, user_settings,
*/ */
@ -21,137 +25,11 @@
printLabels, printLabels,
*/ */
/** const defaultLabelTemplates = {
* Present the user with the available labels, part: user_settings.DEFAULT_PART_LABEL_TEMPLATE,
* and allow them to select which label to print. location: user_settings.DEFAULT_LOCATION_LABEL_TEMPLATE,
* item: user_settings.DEFAULT_ITEM_LABEL_TEMPLATE,
* The intent is that the available labels have been requested line: user_settings.DEFAULT_LINE_LABEL_TEMPLATE,
* (via AJAX) from the server.
*/
function selectLabel(labels, items, options={}) {
// Array of available plugins for label printing
var plugins = [];
// Request a list of available label printing plugins from the server
inventreeGet(
`/api/plugins/`,
{
mixin: 'labels',
},
{
async: false,
success: function(response) {
plugins = response;
}
}
);
var plugin_selection = '';
if (plugins.length > 0) {
plugin_selection =`
<div class='form-group'>
<label class='control-label requiredField' for='id_plugin'>
{% trans "Select Printer" %}
</label>
<div class='controls'>
<select id='id_plugin' class='select form-control' name='plugin'>
`;
plugins.forEach(function(plugin) {
var selected = '';
if (user_settings['LABEL_DEFAULT_PRINTER'] == plugin.key) {
selected = ' selected';
}
plugin_selection += `<option value='${plugin.key}' title='${plugin.meta.human_name}'${selected}>${plugin.name} - <small>${plugin.meta.human_name}</small></option>`;
});
plugin_selection += `
</select>
</div>
</div>
`;
}
var modal = options.modal || '#modal-form';
var label_list = makeOptionsList(
labels,
function(item) {
var text = item.name;
if (item.description) {
text += ` - ${item.description}`;
}
return text;
},
function(item) {
return item.pk;
},
null,
function(item) {
if (options.key == 'part')
return item.pk == user_settings.DEFAULT_PART_LABEL_TEMPLATE;
else if (options.key == 'location')
return item.pk == user_settings.DEFAULT_LOCATION_LABEL_TEMPLATE;
else if (options.key == 'item')
return item.pk == user_settings.DEFAULT_ITEM_LABEL_TEMPLATE;
return '';
}
);
// Construct form
var html = '';
if (items.length > 0) {
let item_name = items.length == 1 ? options.singular_name : options.plural_name;
html += `
<div class='alert alert-block alert-info'>
${items.length} ${item_name} {% trans "selected" %}
</div>`;
}
html += `
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
<div class='form-group'>
<label class='control-label requiredField' for='id_label'>
{% trans "Select Label Template" %}
</label>
<div class='controls'>
<select id='id_label' class='select form-control' name='label'>
${label_list}
</select>
</div>
</div>
${plugin_selection}
</form>`;
openModal({
modal: modal,
});
modalEnable(modal, true);
modalShowSubmitButton(modal, true);
modalSetTitle(modal, '{% trans "Select Label Template" %}');
modalSetContent(modal, html);
attachSelect(modal);
modalSubmit(modal, function() {
var label = $(modal).find('#id_label').val();
var plugin = $(modal).find('#id_plugin').val();
closeModal(modal);
if (options.success) {
options.success({
// Return the selected label template and plugin
label: label,
plugin: plugin,
});
}
});
} }
@ -166,6 +44,7 @@ function selectLabel(labels, items, options={}) {
* - url: The list URL for the particular template type * - url: The list URL for the particular template type
* - items: The list of items to be printed * - items: The list of items to be printed
* - key: The key to use in the query parameters * - key: The key to use in the query parameters
* - plural_name: The plural name of the item type
*/ */
function printLabels(options) { function printLabels(options) {
@ -183,9 +62,11 @@ function printLabels(options) {
params[options.key] = options.items; params[options.key] = options.items;
// Request a list of available label templates // Request a list of available label templates from the server
let labelTemplates = [];
inventreeGet(options.url, params, { inventreeGet(options.url, params, {
success: function(response) { async: false,
success: function (response) {
if (response.length == 0) { if (response.length == 0) {
showAlertDialog( showAlertDialog(
'{% trans "No Labels Found" %}', '{% trans "No Labels Found" %}',
@ -194,34 +75,121 @@ function printLabels(options) {
return; return;
} }
// Select label template for printing labelTemplates = response;
selectLabel(response, options.items, {
success: function(data) {
let href = `${options.url}${data.label}/print/?`;
options.items.forEach(function(item) {
href += `${options.key}=${item}&`;
});
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',
});
}
}
});
},
plural_name: options.plural_name,
singular_name: options.singular_name,
key: options.key,
});
} }
}); });
// Request a list of available label printing plugins from the server
let plugins = [];
inventreeGet(`/api/plugins/`, { mixin: 'labels' }, {
async: false,
success: function (response) {
plugins = response;
}
});
let header_html = "";
// show how much items are selected if there is more than one item selected
if (options.items.length > 1) {
header_html += `
<div class='alert alert-block alert-info'>
${options.items.length} ${options.plural_name} {% trans "selected" %}
</div>
`;
}
const updateFormUrl = (formOptions) => {
const plugin = getFormFieldValue("_plugin", formOptions.fields._plugin, formOptions);
const labelTemplate = getFormFieldValue("_label_template", formOptions.fields._label_template, formOptions);
const params = $.param({ plugin, [options.key]: options.items })
formOptions.url = `${options.url}${labelTemplate ?? "1"}/print/?${params}`;
}
const updatePrintingOptions = (formOptions) => {
let printingOptionsRes = null;
$.ajax({
url: formOptions.url,
type: "OPTIONS",
contentType: "application/json",
dataType: "json",
accepts: { json: "application/json" },
async: false,
success: (res) => { printingOptionsRes = res },
error: (xhr) => showApiError(xhr, formOptions.url)
});
const printingOptions = printingOptionsRes.actions.POST || {};
// clear all other options
formOptions.fields = {
_label_template: formOptions.fields._label_template,
_plugin: formOptions.fields._plugin,
}
if (Object.keys(printingOptions).length > 0) {
formOptions.fields = {
...formOptions.fields,
divider: { type: "candy", html: `<hr/><h5>{% trans "Printing Options" %}</h5>` },
...printingOptions,
};
}
// update form
updateForm(formOptions);
}
const printingFormOptions = {
title: options.items.length === 1 ? `{% trans "Print label" %}` : `{% trans "Print labels" %}`,
submitText: `{% trans "Print" %}`,
method: "POST",
disableSuccessMessage: true,
header_html,
fields: {
_label_template: {
label: `{% trans "Select label template" %}`,
type: "choice",
localOnly: true,
value: defaultLabelTemplates[options.key],
choices: labelTemplates.map(t => ({
value: t.pk,
display_name: `${t.name} - <small>${t.description}</small>`,
})),
onEdit: (_value, _name, _field, formOptions) => {
updateFormUrl(formOptions);
}
},
_plugin: {
label: `{% trans "Select plugin" %}`,
type: "choice",
localOnly: true,
value: user_settings.LABEL_DEFAULT_PRINTER || plugins[0].key,
choices: plugins.map(p => ({
value: p.key,
display_name: `${p.name} - <small>${p.meta.human_name}</small>`,
})),
onEdit: (_value, _name, _field, formOptions) => {
updateFormUrl(formOptions);
updatePrintingOptions(formOptions);
}
},
},
onSuccess: (response) => {
if (response.file) {
// Download the generated file
window.open(response.file);
} else {
showMessage('{% trans "Labels sent to printer" %}', {
style: 'success',
});
}
}
};
// construct form
constructForm(null, printingFormOptions);
// fetch the options for the default plugin
updateFormUrl(printingFormOptions);
updatePrintingOptions(printingFormOptions);
} }

View File

@ -63,6 +63,32 @@ If the `print_labels` method is not changed, this will run the `print_label` met
!!! tip "Custom Code" !!! 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. 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.
### Printing options
A printing plugin can define custom options as a serializer class called `PrintingOptionsSerializer` that get shown on the printing screen and get passed to the `print_labels`/`print_label` function as a kwarg called `printing_options`. This can be used to e.g. let the user dynamically select the orientation of the label, the color mode, ... for each print job.
The following simple example shows how to implement an orientation select. For more information about how to define fields, refer to the django rest framework (DRF) [documentation](https://www.django-rest-framework.org/api-guide/fields/).
```py
from rest_framework import serializers
class MyLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
...
class PrintingOptionsSerializer(serializers.Serializer):
orientation = serializers.ChoiceField(choices=[
("landscape", "Landscape"),
("portrait", "Portrait"),
])
def print_label(self, **kwargs):
print(kwargs["printing_options"]) # -> {"orientation": "landscape"}
...
```
!!! tip "Dynamically return a serializer instance"
If your plugin wants to dynamically expose options based on the request, you can implement the `get_printing_options_serializer` function which by default returns an instance
of the `PrintingOptionsSerializer` class if defined.
### Helper Methods ### Helper Methods
The plugin class provides a number of additional helper methods which may be useful for generating labels: The plugin class provides a number of additional helper methods which may be useful for generating labels:
@ -122,13 +148,15 @@ class MyLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
Send the label to the printer Send the label to the printer
kwargs: kwargs:
pdf_data: An in-memory PDF file of the label pdf_file: The PDF file object of the rendered label (WeasyTemplateResponse object)
png_file: An in-memory PIL (pillow) Image file of the label pdf_data: Raw PDF data of the rendered label
filename: The filename of the printed label (if applicable) filename: The filename of this PDF label
label_instance: The Label model instance label_instance: The instance of the label model which triggered the print_label() method
width: width of the label (in mm) item_instance: The instance of the database model against which the label is printed
height: height of the label (in mm) user: The user who triggered this print job
user: The user who printed this label width: The expected width of the label (in mm)
height: The expected height of the label (in mm)
printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
""" """
width = kwargs['width'] width = kwargs['width']