mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
59f17a9885
commit
a11418398f
@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
- 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
|
||||
- Adds "delivery_date" to shipments
|
||||
>>>>>>> inventree/master
|
||||
|
||||
v113 -> 2023-05-13 : https://github.com/inventree/InvenTree/pull/4800
|
||||
- Adds API endpoints for scrapping a build output
|
||||
|
@ -84,6 +84,10 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
'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
|
||||
for method, permission in rolemap.items():
|
||||
|
||||
|
@ -61,6 +61,10 @@ class RolePermission(permissions.BasePermission):
|
||||
'DELETE': 'delete',
|
||||
}
|
||||
|
||||
# let the view define a custom rolemap
|
||||
if hasattr(view, "rolemap"):
|
||||
rolemap.update(view.rolemap)
|
||||
|
||||
permission = rolemap[request.method]
|
||||
|
||||
# The required role may be defined for the view class
|
||||
|
@ -7,7 +7,9 @@ from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page, never_cache
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.request import clone_request
|
||||
|
||||
import build.models
|
||||
import common.models
|
||||
@ -136,17 +138,45 @@ class LabelListView(LabelFilterMixin, ListAPI):
|
||||
class LabelPrintMixin(LabelFilterMixin):
|
||||
"""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)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""Prevent caching when printing report templates"""
|
||||
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):
|
||||
"""Perform a GET request against this endpoint to print labels"""
|
||||
common.models.InvenTreeUserSetting.set_setting('DEFAULT_' + self.ITEM_KEY.upper() + '_LABEL_TEMPLATE',
|
||||
self.get_object().pk, None, user=request.user)
|
||||
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):
|
||||
"""Return the label printing plugin associated with this request.
|
||||
|
||||
@ -167,14 +197,18 @@ class LabelPrintMixin(LabelFilterMixin):
|
||||
|
||||
plugin = registry.get_plugin(plugin_key)
|
||||
|
||||
if 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:
|
||||
if not plugin:
|
||||
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):
|
||||
"""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
|
||||
@ -187,10 +221,14 @@ class LabelPrintMixin(LabelFilterMixin):
|
||||
# Label template
|
||||
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.
|
||||
# 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):
|
||||
result['plugin'] = plugin.plugin_slug()
|
||||
|
@ -1,8 +1,12 @@
|
||||
"""Plugin mixin classes for label plugins."""
|
||||
|
||||
from typing import Union
|
||||
|
||||
from django.http import JsonResponse
|
||||
|
||||
import pdf2image
|
||||
from rest_framework import serializers
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
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
|
||||
|
||||
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
|
||||
@ -70,7 +74,7 @@ class LabelPrintingMixin:
|
||||
png = pdf2image.convert_from_bytes(pdf_data, dpi=dpi)[0]
|
||||
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.
|
||||
|
||||
Arguments:
|
||||
@ -78,6 +82,9 @@ class LabelPrintingMixin:
|
||||
items: The list of database items to print (e.g. StockItem instances)
|
||||
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:
|
||||
A JSONResponse object which indicates outcome to the user
|
||||
|
||||
@ -107,6 +114,7 @@ class LabelPrintingMixin:
|
||||
'user': user,
|
||||
'width': label.width,
|
||||
'height': label.height,
|
||||
'printing_options': printing_options,
|
||||
}
|
||||
|
||||
if self.BLOCKING_PRINT:
|
||||
@ -136,6 +144,7 @@ class LabelPrintingMixin:
|
||||
user: The user who triggered this print job
|
||||
width: The expected width of the label (in mm)
|
||||
height: The expected height of the label (in mm)
|
||||
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.
|
||||
"""
|
||||
@ -158,3 +167,21 @@ class LabelPrintingMixin:
|
||||
self.plugin_slug(),
|
||||
**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)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
from django.apps import apps
|
||||
from django.urls import reverse
|
||||
@ -183,6 +184,32 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
||||
# And that it is a valid image file
|
||||
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):
|
||||
"""Cover the endpoints not covered by `test_printing_process`."""
|
||||
plugin_ref = 'samplelabelprinter'
|
||||
|
@ -3,6 +3,8 @@
|
||||
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.mixins import LabelPrintingMixin
|
||||
|
||||
@ -17,6 +19,10 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
|
||||
AUTHOR = "InvenTree contributors"
|
||||
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):
|
||||
"""Sample printing step.
|
||||
|
||||
|
@ -32,6 +32,7 @@
|
||||
setFormGroupVisibility,
|
||||
showFormInput,
|
||||
selectImportFields,
|
||||
updateForm,
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -306,6 +307,7 @@ function constructDeleteForm(fields, options) {
|
||||
* - hidden: Set to true to hide the field
|
||||
* - icon: font-awesome icon 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
|
||||
* - focus: Name of field to focus on when modal is displayed
|
||||
* - 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
|
||||
* - confirm: Set to true to require a "confirm" button
|
||||
* - 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={}) {
|
||||
@ -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
|
||||
// The "submit" button will be disabled unless "confirm" is checked
|
||||
@ -841,6 +859,7 @@ function submitFormData(fields, options) {
|
||||
|
||||
// Ignore visual fields
|
||||
if (field && field.type == 'candy') continue;
|
||||
if (field && field.localOnly === true) continue;
|
||||
|
||||
if (field) {
|
||||
|
||||
@ -1190,7 +1209,7 @@ function handleFormSuccess(response, options) {
|
||||
}
|
||||
|
||||
// Display any messages
|
||||
if (response && (response.success || options.successMessage)) {
|
||||
if (!options.disableSuccessMessage && response && (response.success || options.successMessage)) {
|
||||
showAlertOrCache(
|
||||
response.success || options.successMessage,
|
||||
cache,
|
||||
|
@ -4,6 +4,8 @@
|
||||
/* globals
|
||||
attachSelect,
|
||||
closeModal,
|
||||
constructForm,
|
||||
getFormFieldValue,
|
||||
inventreeGet,
|
||||
makeOptionsList,
|
||||
modalEnable,
|
||||
@ -13,7 +15,9 @@
|
||||
modalSubmit,
|
||||
openModal,
|
||||
showAlertDialog,
|
||||
showApiError,
|
||||
showMessage,
|
||||
updateForm,
|
||||
user_settings,
|
||||
*/
|
||||
|
||||
@ -21,137 +25,11 @@
|
||||
printLabels,
|
||||
*/
|
||||
|
||||
/**
|
||||
* Present the user with the available labels,
|
||||
* and allow them to select which label to print.
|
||||
*
|
||||
* The intent is that the available labels have been requested
|
||||
* (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,
|
||||
});
|
||||
}
|
||||
});
|
||||
const defaultLabelTemplates = {
|
||||
part: user_settings.DEFAULT_PART_LABEL_TEMPLATE,
|
||||
location: user_settings.DEFAULT_LOCATION_LABEL_TEMPLATE,
|
||||
item: user_settings.DEFAULT_ITEM_LABEL_TEMPLATE,
|
||||
line: user_settings.DEFAULT_LINE_LABEL_TEMPLATE,
|
||||
}
|
||||
|
||||
|
||||
@ -166,6 +44,7 @@ function selectLabel(labels, items, options={}) {
|
||||
* - url: The list URL for the particular template type
|
||||
* - items: The list of items to be printed
|
||||
* - key: The key to use in the query parameters
|
||||
* - plural_name: The plural name of the item type
|
||||
*/
|
||||
function printLabels(options) {
|
||||
|
||||
@ -183,9 +62,11 @@ function printLabels(options) {
|
||||
|
||||
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, {
|
||||
success: function(response) {
|
||||
async: false,
|
||||
success: function (response) {
|
||||
if (response.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "No Labels Found" %}',
|
||||
@ -194,34 +75,121 @@ function printLabels(options) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select label template for printing
|
||||
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,
|
||||
});
|
||||
labelTemplates = response;
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
@ -63,6 +63,32 @@ If the `print_labels` method is not changed, this will run the `print_label` met
|
||||
!!! 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.
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
|
||||
kwargs:
|
||||
pdf_data: An in-memory PDF file of the label
|
||||
png_file: An in-memory PIL (pillow) Image file of the label
|
||||
filename: The filename of the printed label (if applicable)
|
||||
label_instance: The Label model instance
|
||||
width: width of the label (in mm)
|
||||
height: height of the label (in mm)
|
||||
user: The user who printed this label
|
||||
pdf_file: The PDF file object of the rendered label (WeasyTemplateResponse object)
|
||||
pdf_data: Raw PDF data of the rendered label
|
||||
filename: The filename of this PDF label
|
||||
label_instance: The instance of the label model which triggered the print_label() method
|
||||
item_instance: The instance of the database model against which the label is printed
|
||||
user: The user who triggered this print job
|
||||
width: The expected width of the label (in mm)
|
||||
height: The expected height of the label (in mm)
|
||||
printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
|
||||
"""
|
||||
|
||||
width = kwargs['width']
|
||||
|
Loading…
Reference in New Issue
Block a user