mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Label sheet printer (#5883)
* Add skeleton for new label sheet plugin * Add custom printing options serializer * Render individual label outputs to HTML * Extract page size and column size * Check label dimensions before printing * Split labels into multiple pages / sheets * Render out multiple labels onto a single sheet * Cleanup base label template - Allow @page style to *not* be generated - Pass through as optional context variable - Check that it still works for single label printing (default behaviour unchanged) - Prevents multiple @page styles from being generated on label sheet output * Fix stylesheets for part labels * Cleanup stock location labels * Cleanup more label templates * Check if label can actually fit on page * Generate output to PDF and return correct response * Update panel.md * Fix unit tests * More unit test fixes
This commit is contained in:
		| @@ -221,6 +221,10 @@ class LabelPrintMixin(LabelFilterMixin): | ||||
|         # Label template | ||||
|         label = self.get_object() | ||||
|  | ||||
|         # Check the label dimensions | ||||
|         if label.width <= 0 or label.height <= 0: | ||||
|             raise ValidationError('Label has invalid dimensions') | ||||
|  | ||||
|         # 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) | ||||
|   | ||||
| @@ -191,10 +191,38 @@ class LabelTemplate(MetadataMixin, models.Model): | ||||
|  | ||||
|         return template_string.render(context) | ||||
|  | ||||
|     def context(self, request): | ||||
|         """Provides context data to the template.""" | ||||
|     def generate_page_style(self, **kwargs): | ||||
|         """Generate @page style for the label template. | ||||
|  | ||||
|         This is inserted at the top of the style block for a given label | ||||
|         """ | ||||
|  | ||||
|         width = kwargs.get('width', self.width) | ||||
|         height = kwargs.get('height', self.height) | ||||
|         margin = kwargs.get('margin', 0) | ||||
|  | ||||
|         return f""" | ||||
|         @page {{ | ||||
|             size: {width}mm {height}mm; | ||||
|             margin: {margin}mm; | ||||
|         }} | ||||
|         """ | ||||
|  | ||||
|     def context(self, request, **kwargs): | ||||
|         """Provides context data to the template. | ||||
|  | ||||
|         Arguments: | ||||
|             request: The HTTP request object | ||||
|             kwargs: Additional keyword arguments | ||||
|         """ | ||||
|  | ||||
|         context = self.get_context_data(request) | ||||
|  | ||||
|         # By default, each label is supplied with '@page' data | ||||
|         # However, it can be excluded, e.g. when rendering a label sheet | ||||
|         if kwargs.get('insert_page_style', True): | ||||
|             context['page_style'] = self.generate_page_style() | ||||
|  | ||||
|         # Add "basic" context data which gets passed to every label | ||||
|         context['base_url'] = get_base_url(request=request) | ||||
|         context['date'] = datetime.datetime.now().date() | ||||
| @@ -213,18 +241,31 @@ class LabelTemplate(MetadataMixin, models.Model): | ||||
|  | ||||
|         return context | ||||
|  | ||||
|     def render_as_string(self, request, **kwargs): | ||||
|         """Render the label to a HTML string. | ||||
|     def render_as_string(self, request, target_object=None, **kwargs): | ||||
|         """Render the label to a HTML string""" | ||||
|  | ||||
|         Useful for debug mode (viewing generated code) | ||||
|         """ | ||||
|         return render_to_string(self.template_name, self.context(request), request) | ||||
|         if target_object: | ||||
|             self.object_to_print = target_object | ||||
|  | ||||
|     def render(self, request, **kwargs): | ||||
|         context = self.context(request, **kwargs) | ||||
|  | ||||
|         return render_to_string( | ||||
|             self.template_name, | ||||
|             context, | ||||
|             request | ||||
|         ) | ||||
|  | ||||
|     def render(self, request, target_object=None, **kwargs): | ||||
|         """Render the label template to a PDF file. | ||||
|  | ||||
|         Uses django-weasyprint plugin to render HTML template | ||||
|         """ | ||||
|  | ||||
|         if target_object: | ||||
|             self.object_to_print = target_object | ||||
|  | ||||
|         context = self.context(request, **kwargs) | ||||
|  | ||||
|         wp = WeasyprintLabelMixin( | ||||
|             request, | ||||
|             self.template_name, | ||||
| @@ -235,7 +276,7 @@ class LabelTemplate(MetadataMixin, models.Model): | ||||
|         ) | ||||
|  | ||||
|         return wp.render_to_response( | ||||
|             self.context(request), | ||||
|             context, | ||||
|             **kwargs | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -16,9 +16,9 @@ Refer to the documentation for a full list of available template variables. | ||||
| } | ||||
|  | ||||
| .qr { | ||||
|   position: absolute; | ||||
|   height: 28mm; | ||||
|   width: 28mm; | ||||
|   position: relative; | ||||
|   top: 0mm; | ||||
|   right: 0mm; | ||||
|   float: right; | ||||
|   | ||||
| @@ -4,15 +4,18 @@ | ||||
|  | ||||
| <head> | ||||
|     <style> | ||||
|         @page { | ||||
|             {% localize off %} | ||||
|             size: {{ width }}mm {{ height }}mm; | ||||
|             {% endlocalize %} | ||||
|             {% block margin %} | ||||
|             margin: 0mm; | ||||
|             {% endblock margin %} | ||||
|         } | ||||
|  | ||||
|         {% block page_style %} | ||||
|         {% if page_style %} | ||||
|         /* @page styling */ | ||||
|         {% localize off %} | ||||
|         {{ page_style }} | ||||
|         {% endlocalize %} | ||||
|         {% endif %} | ||||
|         {% endblock page_style %} | ||||
|  | ||||
|         {% block body_style %} | ||||
|         /* body styling */ | ||||
|         body { | ||||
|             font-family: Arial, Helvetica, sans-serif; | ||||
|             margin: 0mm; | ||||
| @@ -21,20 +24,27 @@ | ||||
|             page-break-before: always; | ||||
|             page-break-after: always; | ||||
|         } | ||||
|         {% endblock body_style %} | ||||
|  | ||||
|         img { | ||||
|             display: inline-block; | ||||
|             image-rendering: pixelated; | ||||
|         } | ||||
|  | ||||
|         /* Global content wrapper div which takes up entire page area */ | ||||
|         .content { | ||||
|             width: 100%; | ||||
|             {% localize off %} | ||||
|             width: {{ width }}mm; | ||||
|             height: {{ height }}mm; | ||||
|             {% endlocalize %} | ||||
|             break-after: always; | ||||
|             position: relative; | ||||
|             left: 0mm; | ||||
|             top: 0mm; | ||||
|         } | ||||
|  | ||||
|         {% block style %} | ||||
|         /* User-defined styles can go here */ | ||||
|         /* User-defined styles can go here, and override any styles defined above */ | ||||
|         {% endblock style %} | ||||
|  | ||||
|     </style> | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| {% block style %} | ||||
|  | ||||
| .qr { | ||||
|     position: fixed; | ||||
|     position: absolute; | ||||
|     left: 0mm; | ||||
|     top: 0mm; | ||||
|     {% localize off %} | ||||
| @@ -16,7 +16,7 @@ | ||||
|  | ||||
| .part { | ||||
|     font-family: Arial, Helvetica, sans-serif; | ||||
|     display: inline; | ||||
|     display: flex; | ||||
|     position: absolute; | ||||
|     {% localize off %} | ||||
|     left: {{ height }}mm; | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| {% block style %} | ||||
|  | ||||
| .qr { | ||||
|     position: fixed; | ||||
|     position: absolute; | ||||
|     left: 0mm; | ||||
|     top: 0mm; | ||||
|     {% localize off %} | ||||
| @@ -16,7 +16,7 @@ | ||||
|  | ||||
| .part { | ||||
|     font-family: Arial, Helvetica, sans-serif; | ||||
|     display: inline; | ||||
|     display: flex; | ||||
|     position: absolute; | ||||
|     {% localize off %} | ||||
|     left: {{ height }}mm; | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| {% block style %} | ||||
|  | ||||
| .qr { | ||||
|     position: fixed; | ||||
|     position: absolute; | ||||
|     left: 0mm; | ||||
|     top: 0mm; | ||||
|     {% localize off %} | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| {% block style %} | ||||
|  | ||||
| .qr { | ||||
|     position: fixed; | ||||
|     position: absolute; | ||||
|     left: 0mm; | ||||
|     top: 0mm; | ||||
|     {% localize off %} | ||||
| @@ -17,6 +17,5 @@ | ||||
| {% endblock style %} | ||||
|  | ||||
| {% block content %} | ||||
|  | ||||
| <img class='qr' alt="{% trans 'QR Code' %}" src='{% qrcode qr_data %}'> | ||||
| {% endblock content %} | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| {% block style %} | ||||
|  | ||||
| .qr { | ||||
|     position: fixed; | ||||
|     position: absolute; | ||||
|     left: 0mm; | ||||
|     top: 0mm; | ||||
|     {% localize off %} | ||||
| @@ -16,7 +16,7 @@ | ||||
|  | ||||
| .loc { | ||||
|     font-family: Arial, Helvetica, sans-serif; | ||||
|     display: inline; | ||||
|     display: flex; | ||||
|     position: absolute; | ||||
|     {% localize off %} | ||||
|     left: {{ height }}mm; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user