2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 11:10:54 +00:00

[Bug] Report fix (#9887)

* Improved mechanisms for data output

- Log errors against output
- Properly catch rendering errors

* Updated error handling

* Fix "render_date" tag

* Update default report template

- Make sure the helper is used

* Bug fixes
This commit is contained in:
Oliver
2025-06-28 08:23:12 +10:00
committed by GitHub
parent b3feebb53b
commit ff6d4bfb8f
10 changed files with 114 additions and 58 deletions

View File

@ -169,17 +169,12 @@ class InvenTreeExceptionProcessor(ExceptionProcessor):
def process_exception(self, request, exception): def process_exception(self, request, exception):
"""Check if kind is ignored before processing.""" """Check if kind is ignored before processing."""
kind, info, data = sys.exc_info() kind, _info, _data = sys.exc_info()
# Check if the error is on the ignore list # Check if the error is on the ignore list
if kind in settings.IGNORED_ERRORS: if kind in settings.IGNORED_ERRORS:
return return
import traceback
from django.views.debug import ExceptionReporter
from error_report.models import Error
from error_report.settings import ERROR_DETAIL_SETTINGS from error_report.settings import ERROR_DETAIL_SETTINGS
# Error reporting is disabled # Error reporting is disabled
@ -194,15 +189,10 @@ class InvenTreeExceptionProcessor(ExceptionProcessor):
if len(path) > 200: if len(path) > 200:
path = path[:195] + '...' path = path[:195] + '...'
error = Error.objects.create( # Pass off to the exception reporter
kind=kind.__name__, from InvenTree.exceptions import log_error
html=ExceptionReporter(request, kind, info, data).get_traceback_html(),
path=path,
info=info,
data='\n'.join(traceback.format_exception(kind, info, data)),
)
error.save() log_error(path)
class InvenTreeRequestCacheMiddleware(MiddlewareMixin): class InvenTreeRequestCacheMiddleware(MiddlewareMixin):

View File

@ -38,7 +38,7 @@ def decimal(x, *args, **kwargs):
return InvenTree.helpers.decimal2string(x) return InvenTree.helpers.decimal2string(x)
@register.simple_tag(takes_context=True) @register.simple_tag()
def render_date(date_object): def render_date(date_object):
"""Renders a date object as a string.""" """Renders a date object as a string."""
if date_object is None: if date_object is None:

View File

@ -24,6 +24,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.humanize.templatetags.humanize import naturaltime from django.contrib.humanize.templatetags.humanize import naturaltime
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.mail.utils import DNS_NAME from django.core.mail.utils import DNS_NAME
@ -2424,6 +2425,39 @@ class DataOutput(models.Model):
errors = models.JSONField(blank=True, null=True) errors = models.JSONField(blank=True, null=True)
def mark_complete(self, progress: int = 100, output: Optional[ContentFile] = None):
"""Mark the data output generation process as complete.
Arguments:
progress (int, optional): Progress percentage of the data output generation. Defaults to 100.
output (ContentFile, optional): The generated output file. Defaults to None.
"""
self.complete = True
self.progress = progress
self.output = output
self.save()
def mark_failure(
self, error: Optional[str] = None, error_dict: Optional[dict] = None
):
"""Log an error message to the errors field.
Arguments:
error (str, optional): Error message to log. Defaults to None.
error_dict (dict): Dictionary containing error messages. Defaults to None.
"""
self.complete = False
self.output = None
if error_dict is not None:
self.errors = error_dict
elif error is not None:
self.errors = {'error': str(error)}
else:
self.errors = {'error': str(_('An error occurred'))}
self.save()
# region Email # region Email
class Priority(models.IntegerChoices): class Priority(models.IntegerChoices):

View File

@ -391,10 +391,7 @@ class DataExportViewMixin:
raise ValidationError(_('Error occurred during data export')) raise ValidationError(_('Error occurred during data export'))
# Update the output object with the exported data # Update the output object with the exported data
output.progress = 100 output.mark_complete(output=ContentFile(datafile, filename))
output.complete = True
output.output = ContentFile(datafile, filename)
output.save()
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Override the GET method to determine export options.""" """Override the GET method to determine export options."""

View File

@ -188,14 +188,10 @@ class LabelPrintingMixin:
output.progress += 1 output.progress += 1
output.save() output.save()
generated_file = self.get_generated_file(**print_args)
# Mark the output as complete # Mark the output as complete
output.complete = True output.mark_complete(progress=N, output=generated_file)
output.progress = N
# Add in the generated file (if applicable)
output.output = self.get_generated_file(**print_args)
output.save()
def get_generated_file(self, **kwargs): def get_generated_file(self, **kwargs):
"""Return the generated file for download (or None, if this plugin does not generate a file output). """Return the generated file for download (or None, if this plugin does not generate a file output).

View File

@ -113,9 +113,7 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin):
# Inform the user that the process has been offloaded to the printer # Inform the user that the process has been offloaded to the printer
if output: if output:
output.output = None output.mark_complete()
output.complete = True
output.save()
class PrintingOptionsSerializer(serializers.Serializer): class PrintingOptionsSerializer(serializers.Serializer):
"""Printing options serializer that adds a machine select and the machines options.""" """Printing options serializer that adds a machine select and the machines options."""

View File

@ -165,17 +165,14 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
if str2bool(self.get_setting('DEBUG')): if str2bool(self.get_setting('DEBUG')):
# In debug mode return with the raw HTML # In debug mode return with the raw HTML
output.output = ContentFile(html_data, 'labels.html') generated_file = ContentFile(html_data, 'labels.html')
else: else:
# Render HTML to PDF # Render HTML to PDF
html = weasyprint.HTML(string=html_data) html = weasyprint.HTML(string=html_data)
document = html.render().write_pdf() document = html.render().write_pdf()
generated_file = ContentFile(document, 'labels.pdf')
output.output = ContentFile(document, 'labels.pdf') output.mark_complete(progress=n_labels, output=generated_file)
output.progress = n_labels
output.complete = True
output.save()
def print_page(self, label: LabelTemplate, items: list, request, **kwargs): def print_page(self, label: LabelTemplate, items: list, request, **kwargs):
"""Generate a single page of labels. """Generate a single page of labels.

View File

@ -14,7 +14,7 @@ from django.core.files.storage import default_storage
from django.core.validators import FileExtensionValidator, MinValueValidator from django.core.validators import FileExtensionValidator, MinValueValidator
from django.db import models from django.db import models
from django.template import Context, Template from django.template import Context, Template
from django.template.exceptions import TemplateDoesNotExist from django.template.exceptions import TemplateDoesNotExist, TemplateSyntaxError
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -46,6 +46,17 @@ except OSError as err: # pragma: no cover
logger = structlog.getLogger('inventree') logger = structlog.getLogger('inventree')
def log_report_error(*args, **kwargs):
"""Log an error message when a report fails to render."""
try:
do_log = get_global_setting('REPORT_LOG_ERRORS', backup_value=True)
except Exception:
do_log = True
if do_log:
InvenTree.exceptions.log_error(*args, **kwargs)
def rename_template(instance, filename): def rename_template(instance, filename):
"""Function to rename a report template once uploaded. """Function to rename a report template once uploaded.
@ -528,7 +539,17 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
report = self.render(instance, request, contexts) report = self.render(instance, request, contexts)
except TemplateDoesNotExist as e: except TemplateDoesNotExist as e:
t_name = str(e) or self.template t_name = str(e) or self.template
raise ValidationError(f'Template file {t_name} does not exist') msg = f'Template file {t_name} does not exist'
output.mark_failure(error=msg)
raise ValidationError(msg)
except TemplateSyntaxError as e:
msg = _('Template syntax error')
output.mark_failure(msg)
raise ValidationError(f'{msg}: {e!s}')
except Exception as e:
msg = _('Error rendering report')
output.mark_failure(msg)
raise ValidationError(f'{msg}: {e!s}')
outputs.append(report) outputs.append(report)
self.handle_attachment( self.handle_attachment(
@ -554,7 +575,17 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
report = self.render(instance, request, None) report = self.render(instance, request, None)
except TemplateDoesNotExist as e: except TemplateDoesNotExist as e:
t_name = str(e) or self.template t_name = str(e) or self.template
raise ValidationError(f'Template file {t_name} does not exist') msg = f'Template file {t_name} does not exist'
output.mark_failure(error=msg)
raise ValidationError(msg)
except TemplateSyntaxError as e:
msg = _('Template syntax error')
output.mark_failure(error=_('Template syntax error'))
raise ValidationError(f'{msg}: {e!s}')
except Exception as e:
msg = _('Error rendering report')
output.mark_failure(error=msg)
raise ValidationError(f'{msg}: {e!s}')
outputs.append(report) outputs.append(report)
@ -569,8 +600,7 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
except Exception as exc: except Exception as exc:
# Something went wrong during the report generation process # Something went wrong during the report generation process
if get_global_setting('REPORT_LOG_ERRORS', backup_value=True): log_report_error('ReportTemplate.print')
InvenTree.exceptions.log_error('print', plugin=self.slug)
raise ValidationError({ raise ValidationError({
'error': _('Error generating report'), 'error': _('Error generating report'),
@ -601,13 +631,15 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
data = pdf_file.getvalue() data = pdf_file.getvalue()
pdf_file.close() pdf_file.close()
except Exception: except Exception:
InvenTree.exceptions.log_error('print', plugin=self.slug) log_report_error('ReportTemplate.print')
data = None msg = _('Error merging report outputs')
output.mark_failure(error=msg)
raise ValidationError(msg)
# Save the generated report to the database # Save the generated report to the database
output.complete = True generated_file = ContentFile(data, report_name)
output.output = ContentFile(data, report_name)
output.save() output.mark_complete(output=generated_file)
return output return output

View File

@ -19,7 +19,14 @@
{% block page_content %} {% block page_content %}
<h3>{% trans "Line Items" %}</h3> <h4>{% trans "Order Details" %}</h4>
<ul>
<li>Issue Date: {% render_date order.issue_date %}</li>
<li>Target Date: {% render_date order.target_date %}</li>
</ul>
<h4>{% trans "Line Items" %}</h4>
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<thead> <thead>

View File

@ -3,7 +3,7 @@ import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useDocumentVisibility } from '@mantine/hooks'; import { useDocumentVisibility } from '@mantine/hooks';
import { notifications, showNotification } from '@mantine/notifications'; import { notifications, showNotification } from '@mantine/notifications';
import { IconCircleCheck } from '@tabler/icons-react'; import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ProgressBar } from '../components/items/ProgressBar'; import { ProgressBar } from '../components/items/ProgressBar';
@ -50,7 +50,22 @@ export default function useDataOutput({
.then((response) => { .then((response) => {
const data = response?.data ?? {}; const data = response?.data ?? {};
if (data.complete) { if (!!data.errors || !!data.error) {
setLoading(false);
const error: string =
data?.error ?? data?.errors?.error ?? t`Process failed`;
notifications.update({
id: `data-output-${id}`,
loading: false,
icon: <IconExclamationCircle />,
autoClose: 2500,
title: title,
message: error,
color: 'red'
});
} else if (data.complete) {
setLoading(false); setLoading(false);
notifications.update({ notifications.update({
id: `data-output-${id}`, id: `data-output-${id}`,
@ -66,16 +81,6 @@ export default function useDataOutput({
const url = generateUrl(data.output); const url = generateUrl(data.output);
window.open(url.toString(), '_blank'); window.open(url.toString(), '_blank');
} }
} else if (!!data.error) {
setLoading(false);
notifications.update({
id: `data-output-${id}`,
loading: false,
autoClose: 2500,
title: title,
message: t`Process failed`,
color: 'red'
});
} else { } else {
notifications.update({ notifications.update({
id: `data-output-${id}`, id: `data-output-${id}`,