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:
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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."""
|
||||||
|
@ -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).
|
||||||
|
@ -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."""
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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}`,
|
||||||
|
Reference in New Issue
Block a user