From 5edf72e0848a286f157fe41b8c41d2ca74f35747 Mon Sep 17 00:00:00 2001 From: Petr Ledvina Date: Tue, 22 Sep 2026 14:12:47 +0200 Subject: [PATCH] Preserve validation details when rendering labels (#12874) * Preserve label rendering validation errors * Preserve label validation diagnostics before output deletion Keep logging validation errors in both rendering wrappers before re-raising the original exception. LabelTemplate.print deletes failed DataOutput records, so the error log preserves the diagnostic after the result is removed. Update the mocked tests to require logging while retaining the original validation details. * Apply Ruff preview formatting to label rendering tests --- .../InvenTree/plugin/base/label/mixins.py | 6 ++ .../plugin/base/label/test_label_mixin.py | 61 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/backend/InvenTree/plugin/base/label/mixins.py b/src/backend/InvenTree/plugin/base/label/mixins.py index 78182e343c..2b1150140f 100644 --- a/src/backend/InvenTree/plugin/base/label/mixins.py +++ b/src/backend/InvenTree/plugin/base/label/mixins.py @@ -51,6 +51,9 @@ class LabelPrintingMixin: """ try: return label.render(instance, request=request, user=user) + except ValidationError: + log_error('render_to_pdf', plugin=self.slug) + raise except Exception: log_error('render_to_pdf', plugin=self.slug) raise ValidationError(_('Error rendering label to PDF')) @@ -68,6 +71,9 @@ class LabelPrintingMixin: """ try: return label.render_as_string(instance, request=request, user=user) + except ValidationError: + log_error('render_to_html', plugin=self.slug) + raise except Exception: log_error('render_to_html', plugin=self.slug) raise ValidationError(_('Error rendering label to HTML')) diff --git a/src/backend/InvenTree/plugin/base/label/test_label_mixin.py b/src/backend/InvenTree/plugin/base/label/test_label_mixin.py index f64734ee53..a75e933c14 100644 --- a/src/backend/InvenTree/plugin/base/label/test_label_mixin.py +++ b/src/backend/InvenTree/plugin/base/label/test_label_mixin.py @@ -5,6 +5,8 @@ import os from unittest import mock from django.apps import apps +from django.core.exceptions import ValidationError +from django.test import SimpleTestCase from django.urls import reverse from pdfminer.high_level import extract_text @@ -24,6 +26,65 @@ from report.tests import PrintTestMixins from stock.models import StockItem, StockLocation +class LabelRenderingTests(SimpleTestCase): + """Test error handling in the label rendering wrappers.""" + + def setUp(self): + """Create a label plugin without loading the plugin registry.""" + + class TestLabelPlugin(LabelPrintingMixin, InvenTreePlugin): + NAME = 'Test Label Printer' + + self.plugin = TestLabelPlugin() + + @mock.patch('plugin.base.label.mixins.log_error') + def test_validation_errors(self, log_error): + """Log validation errors while preserving messages, codes, and parameters.""" + for wrapper, renderer in [ + ('render_to_pdf', 'render'), + ('render_to_html', 'render_as_string'), + ]: + with self.subTest(wrapper=wrapper): + log_error.reset_mock() + error = ValidationError({ + 'serial': ValidationError( + 'Missing serial number for %(part)s', + code='missing_serial', + params={'part': 'Test part'}, + ) + }) + label = mock.Mock(spec=LabelTemplate) + getattr(label, renderer).side_effect = error + + with self.assertRaises(ValidationError) as raised: + getattr(self.plugin, wrapper)(label, mock.sentinel.instance, None) + + self.assertIs(raised.exception, error) + self.assertEqual( + raised.exception.message_dict, + {'serial': ['Missing serial number for Test part']}, + ) + log_error.assert_called_once_with(wrapper, plugin=self.plugin.slug) + + @mock.patch('plugin.base.label.mixins.log_error') + def test_unexpected_errors(self, log_error): + """Log unexpected errors and return the existing generic messages.""" + for wrapper, renderer, message in [ + ('render_to_pdf', 'render', 'Error rendering label to PDF'), + ('render_to_html', 'render_as_string', 'Error rendering label to HTML'), + ]: + with self.subTest(wrapper=wrapper): + log_error.reset_mock() + label = mock.Mock(spec=LabelTemplate) + getattr(label, renderer).side_effect = RuntimeError('Rendering failed') + + with self.assertRaises(ValidationError) as raised: + getattr(self.plugin, wrapper)(label, mock.sentinel.instance, None) + + self.assertEqual(raised.exception.messages, [message]) + log_error.assert_called_once_with(wrapper, plugin=self.plugin.slug) + + class LabelMixinTests(PrintTestMixins, InvenTreeAPITestCase): """Test that the Label mixin operates correctly."""