Coverage for src/backend/InvenTree/plugin/base/label/mixins.py: 38%
74 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Plugin mixin classes for label plugins."""
3from django.core.exceptions import ValidationError
4from django.utils.translation import gettext_lazy as _
6import pdf2image
7from rest_framework import serializers
8from rest_framework.request import Request
10from common.models import DataOutput, InvenTreeSetting
11from InvenTree.exceptions import log_error
12from InvenTree.tasks import offload_task
13from plugin import PluginMixinEnum
14from plugin.base.label import label as plugin_label
15from plugin.helpers import MixinNotImplementedError
16from report.models import LabelTemplate
19class LabelPrintingMixin:
20 """Mixin which enables direct printing of stock labels.
22 Each plugin must provide a NAME attribute, which is used to uniquely identify the printer.
24 The plugin *must* also implement the print_label() function for rendering an individual label
26 Note that the print_labels() function can also be overridden to provide custom behavior.
27 """
29 class MixinMeta:
30 """Meta options for this mixin."""
32 MIXIN_NAME = 'Label printing'
34 def __init__(self): # pragma: no cover
35 """Register mixin."""
36 super().__init__()
37 self.add_mixin(PluginMixinEnum.LABELS, True, __class__)
39 BLOCKING_PRINT = True
41 def render_to_pdf(
42 self, label: LabelTemplate, instance, request, user=None, **kwargs
43 ):
44 """Render this label to PDF format.
46 Arguments:
47 label: The LabelTemplate object to render against
48 instance: The model instance to render
49 request: The HTTP request object which triggered this print job (optional, may be None)
50 user: The user who triggered this print job (optional, may be None)
51 """
52 try:
53 return label.render(instance, request=request, user=user)
54 except Exception:
55 log_error('render_to_pdf', plugin=self.slug)
56 raise ValidationError(_('Error rendering label to PDF'))
58 def render_to_html(
59 self, label: LabelTemplate, instance, request, user=None, **kwargs
60 ):
61 """Render this label to HTML format.
63 Arguments:
64 label: The LabelTemplate object to render against
65 instance: The model instance to render
66 request: The HTTP request object which triggered this print job (optional, may be None)
67 user: The user who triggered this print job (optional, may be None)
68 """
69 try:
70 return label.render_as_string(instance, request=request, user=user)
71 except Exception:
72 log_error('render_to_html', plugin=self.slug)
73 raise ValidationError(_('Error rendering label to HTML'))
75 def render_to_png(
76 self, label: LabelTemplate, instance, request=None, user=None, **kwargs
77 ):
78 """Render this label to PNG format.
80 Arguments:
81 label: The LabelTemplate object to render against
82 instance: The model instance to render
83 request: The HTTP request object which triggered this print job (optional, may be None)
84 user: The user who triggered this print job (optional, may be None)
86 Keyword Arguments:
87 pdf_data: The raw PDF data of the rendered label (if already rendered)
88 dpi: The DPI to use for the PNG rendering
89 use_cairo (bool): Whether to use the pdftocairo backend for rendering which provides better results in tests,
90 see [#6488](https://github.com/inventree/InvenTree/pull/6488) for details. If False, pdftoppm is used (default: True)
91 pdf2image_kwargs (dict): Additional keyword arguments to pass to the
92 [`pdf2image.convert_from_bytes`](https://pdf2image.readthedocs.io/en/latest/reference.html#pdf2image.pdf2image.convert_from_bytes) method (optional)
93 """
94 # Check if pdf data is provided
95 pdf_data = kwargs.get('pdf_data')
97 if not pdf_data:
98 pdf_data = self.render_to_pdf(label, instance, request, user=user, **kwargs)
100 pdf2image_kwargs = {
101 'dpi': kwargs.get('dpi', InvenTreeSetting.get_setting('LABEL_DPI', 300)),
102 'use_pdftocairo': kwargs.get('use_cairo', True),
103 **kwargs.get('pdf2image_kwargs', {}),
104 }
106 # Convert to png data
107 try:
108 return pdf2image.convert_from_bytes(pdf_data, **pdf2image_kwargs)[0]
109 except Exception:
110 log_error('render_to_png', plugin=self.slug)
111 return None
113 def print_labels(
114 self,
115 label: LabelTemplate,
116 output: DataOutput,
117 items: list,
118 request: Request,
119 **kwargs,
120 ) -> None:
121 """Print one or more labels with the provided template and items.
123 Arguments:
124 label: The LabelTemplate object to use for printing
125 output: The DataOutput object used to store the results
126 items: The list of database items to print (e.g. StockItem instances)
127 request: The HTTP request object which triggered this print job
129 Keyword Arguments:
130 printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
132 Returns:
133 None. Output data should be stored in the provided DataOutput object
135 Raises:
136 ValidationError if there is an error during the print process
138 The default implementation simply calls print_label() for each label, producing multiple single label output "jobs"
139 but this can be overridden by the particular plugin.
140 """
141 # Extract user information, in decreasing order of preference
142 user = (
143 kwargs.pop('user', None)
144 or getattr(request, 'user', None)
145 or getattr(output, 'user', None)
146 )
148 # Initial state for the output print job
149 output.progress = 0
150 output.complete = False
151 output.save()
153 N = len(items)
155 if N <= 0:
156 raise ValidationError(_('No items provided to print'))
158 # Generate a label output for each provided item
159 for item in items:
160 context = label.get_context(item, request, user=user)
161 filename = label.generate_filename(context)
162 pdf_data = self.render_to_pdf(label, item, request, user=user, **kwargs)
163 png_file = self.render_to_png(
164 label, item, request, pdf_data=pdf_data, user=user, **kwargs
165 )
167 print_args = {
168 'pdf_data': pdf_data,
169 'png_file': png_file,
170 'filename': filename,
171 'context': context,
172 'output': output,
173 'label_instance': label,
174 'item_instance': item,
175 'user': user,
176 'width': label.width,
177 'height': label.height,
178 'printing_options': kwargs['printing_options'],
179 }
181 if self.BLOCKING_PRINT:
182 # Print the label (blocking)
183 self.print_label(**print_args)
184 else:
185 # Offload the print task to the background worker
187 # Exclude the 'context' object - cannot be pickled
188 print_args.pop('context', None)
190 offload_task(
191 plugin_label.print_label,
192 self.plugin_slug(),
193 group='plugin',
194 **print_args,
195 )
197 # Update the progress of the print job
198 output.progress += 1
199 output.save()
201 generated_file = self.get_generated_file(**print_args)
203 # Mark the output as complete
204 output.mark_complete(progress=N, output=generated_file)
206 def get_generated_file(self, **kwargs):
207 """Return the generated file for download (or None, if this plugin does not generate a file output).
209 The default implementation returns None, but this can be overridden by the particular plugin.
210 """
211 return None
213 def print_label(self, **kwargs):
214 """Print a single label (blocking).
216 kwargs:
217 pdf_data: Raw PDF data of the rendered label
218 filename: The filename of this PDF label
219 label_instance: The instance of the label model which triggered the print_label() method
220 item_instance: The instance of the database model against which the label is printed
221 output: The DataOutput object used to store the results of the print job
222 user: The user who triggered this print job
223 width: The expected width of the label (in mm)
224 height: The expected height of the label (in mm)
225 printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
227 Note that the supplied kwargs may be different if the plugin overrides the print_labels() method.
228 """
229 # Unimplemented (to be implemented by the particular plugin class)
230 raise MixinNotImplementedError(
231 'This Plugin must implement a `print_label` method'
232 )
234 def get_printing_options_serializer(
235 self, request: Request, *args, **kwargs
236 ) -> serializers.Serializer | None:
237 """Return a serializer class instance with dynamic printing options.
239 Arguments:
240 request: The request made to print a label or interfering the available serializer fields via an OPTIONS request
241 *args, **kwargs: need to be passed to the serializer instance
243 Returns:
244 A class instance of a DRF serializer class, by default this an instance of
245 self.PrintingOptionsSerializer using the *args, **kwargs if existing for this plugin
246 """
247 serializer = getattr(self, 'PrintingOptionsSerializer', None)
249 if not serializer: 249 ↛ 252line 249 didn't jump to line 252 because the condition on line 249 was always true
250 return None
252 return serializer(*args, **kwargs)
254 def before_printing(self):
255 """Hook method called before printing labels."""
257 def after_printing(self):
258 """Hook method called after printing labels."""