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

1"""Plugin mixin classes for label plugins.""" 

2 

3from django.core.exceptions import ValidationError 

4from django.utils.translation import gettext_lazy as _ 

5 

6import pdf2image 

7from rest_framework import serializers 

8from rest_framework.request import Request 

9 

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 

17 

18 

19class LabelPrintingMixin: 

20 """Mixin which enables direct printing of stock labels. 

21 

22 Each plugin must provide a NAME attribute, which is used to uniquely identify the printer. 

23 

24 The plugin *must* also implement the print_label() function for rendering an individual label 

25 

26 Note that the print_labels() function can also be overridden to provide custom behavior. 

27 """ 

28 

29 class MixinMeta: 

30 """Meta options for this mixin.""" 

31 

32 MIXIN_NAME = 'Label printing' 

33 

34 def __init__(self): # pragma: no cover 

35 """Register mixin.""" 

36 super().__init__() 

37 self.add_mixin(PluginMixinEnum.LABELS, True, __class__) 

38 

39 BLOCKING_PRINT = True 

40 

41 def render_to_pdf( 

42 self, label: LabelTemplate, instance, request, user=None, **kwargs 

43 ): 

44 """Render this label to PDF format. 

45 

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')) 

57 

58 def render_to_html( 

59 self, label: LabelTemplate, instance, request, user=None, **kwargs 

60 ): 

61 """Render this label to HTML format. 

62 

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')) 

74 

75 def render_to_png( 

76 self, label: LabelTemplate, instance, request=None, user=None, **kwargs 

77 ): 

78 """Render this label to PNG format. 

79 

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) 

85 

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') 

96 

97 if not pdf_data: 

98 pdf_data = self.render_to_pdf(label, instance, request, user=user, **kwargs) 

99 

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 } 

105 

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 

112 

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. 

122 

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 

128 

129 Keyword Arguments: 

130 printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer 

131 

132 Returns: 

133 None. Output data should be stored in the provided DataOutput object 

134 

135 Raises: 

136 ValidationError if there is an error during the print process 

137 

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 ) 

147 

148 # Initial state for the output print job 

149 output.progress = 0 

150 output.complete = False 

151 output.save() 

152 

153 N = len(items) 

154 

155 if N <= 0: 

156 raise ValidationError(_('No items provided to print')) 

157 

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 ) 

166 

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 } 

180 

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 

186 

187 # Exclude the 'context' object - cannot be pickled 

188 print_args.pop('context', None) 

189 

190 offload_task( 

191 plugin_label.print_label, 

192 self.plugin_slug(), 

193 group='plugin', 

194 **print_args, 

195 ) 

196 

197 # Update the progress of the print job 

198 output.progress += 1 

199 output.save() 

200 

201 generated_file = self.get_generated_file(**print_args) 

202 

203 # Mark the output as complete 

204 output.mark_complete(progress=N, output=generated_file) 

205 

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). 

208 

209 The default implementation returns None, but this can be overridden by the particular plugin. 

210 """ 

211 return None 

212 

213 def print_label(self, **kwargs): 

214 """Print a single label (blocking). 

215 

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 

226 

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 ) 

233 

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. 

238 

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 

242 

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) 

248 

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 

251 

252 return serializer(*args, **kwargs) 

253 

254 def before_printing(self): 

255 """Hook method called before printing labels.""" 

256 

257 def after_printing(self): 

258 """Hook method called after printing labels."""