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
|
||||||
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
|
||||||
|
@ -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():
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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']
|
||||||
|
Loading…
Reference in New Issue
Block a user