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):
"""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
if kind in settings.IGNORED_ERRORS:
return
import traceback
from django.views.debug import ExceptionReporter
from error_report.models import Error
from error_report.settings import ERROR_DETAIL_SETTINGS
# Error reporting is disabled
@ -194,15 +189,10 @@ class InvenTreeExceptionProcessor(ExceptionProcessor):
if len(path) > 200:
path = path[:195] + '...'
error = Error.objects.create(
kind=kind.__name__,
html=ExceptionReporter(request, kind, info, data).get_traceback_html(),
path=path,
info=info,
data='\n'.join(traceback.format_exception(kind, info, data)),
)
# Pass off to the exception reporter
from InvenTree.exceptions import log_error
error.save()
log_error(path)
class InvenTreeRequestCacheMiddleware(MiddlewareMixin):

View File

@ -38,7 +38,7 @@ def decimal(x, *args, **kwargs):
return InvenTree.helpers.decimal2string(x)
@register.simple_tag(takes_context=True)
@register.simple_tag()
def render_date(date_object):
"""Renders a date object as a string."""
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.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.mail.utils import DNS_NAME
@ -2424,6 +2425,39 @@ class DataOutput(models.Model):
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
class Priority(models.IntegerChoices):

View File

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

View File

@ -188,14 +188,10 @@ class LabelPrintingMixin:
output.progress += 1
output.save()
generated_file = self.get_generated_file(**print_args)
# Mark the output as complete
output.complete = True
output.progress = N
# Add in the generated file (if applicable)
output.output = self.get_generated_file(**print_args)
output.save()
output.mark_complete(progress=N, output=generated_file)
def get_generated_file(self, **kwargs):
"""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
if output:
output.output = None
output.complete = True
output.save()
output.mark_complete()
class PrintingOptionsSerializer(serializers.Serializer):
"""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')):
# In debug mode return with the raw HTML
output.output = ContentFile(html_data, 'labels.html')
generated_file = ContentFile(html_data, 'labels.html')
else:
# Render HTML to PDF
html = weasyprint.HTML(string=html_data)
document = html.render().write_pdf()
generated_file = ContentFile(document, 'labels.pdf')
output.output = ContentFile(document, 'labels.pdf')
output.progress = n_labels
output.complete = True
output.save()
output.mark_complete(progress=n_labels, output=generated_file)
def print_page(self, label: LabelTemplate, items: list, request, **kwargs):
"""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.db import models
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.urls import reverse
from django.utils.translation import gettext_lazy as _
@ -46,6 +46,17 @@ except OSError as err: # pragma: no cover
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):
"""Function to rename a report template once uploaded.
@ -528,7 +539,17 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
report = self.render(instance, request, contexts)
except TemplateDoesNotExist as e:
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)
self.handle_attachment(
@ -554,7 +575,17 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
report = self.render(instance, request, None)
except TemplateDoesNotExist as e:
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)
@ -569,8 +600,7 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
except Exception as exc:
# Something went wrong during the report generation process
if get_global_setting('REPORT_LOG_ERRORS', backup_value=True):
InvenTree.exceptions.log_error('print', plugin=self.slug)
log_report_error('ReportTemplate.print')
raise ValidationError({
'error': _('Error generating report'),
@ -601,13 +631,15 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
data = pdf_file.getvalue()
pdf_file.close()
except Exception:
InvenTree.exceptions.log_error('print', plugin=self.slug)
data = None
log_report_error('ReportTemplate.print')
msg = _('Error merging report outputs')
output.mark_failure(error=msg)
raise ValidationError(msg)
# Save the generated report to the database
output.complete = True
output.output = ContentFile(data, report_name)
output.save()
generated_file = ContentFile(data, report_name)
output.mark_complete(output=generated_file)
return output

View File

@ -19,7 +19,14 @@
{% 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'>
<thead>

View File

@ -3,7 +3,7 @@ import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro';
import { useDocumentVisibility } from '@mantine/hooks';
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 { useEffect, useState } from 'react';
import { ProgressBar } from '../components/items/ProgressBar';
@ -50,7 +50,22 @@ export default function useDataOutput({
.then((response) => {
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);
notifications.update({
id: `data-output-${id}`,
@ -66,16 +81,6 @@ export default function useDataOutput({
const url = generateUrl(data.output);
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 {
notifications.update({
id: `data-output-${id}`,