mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-17 23:08:28 +00:00
[bug] Fix for report generation context (#11941)
* Provide explicit user information to report context * Pass user information through to offloaded task * Pass user information back to the plugin * Fix user passing * Add more complex unit test for report printing - Test debug ON / OFF - Test merge ON / OFF - Check generated data output * Unit testing for label generation * Further refactoring - Remove "request" from printing context entirely * Simplify argument drilling * Robustify unit tests
This commit is contained in:
@@ -24,7 +24,7 @@ class ReportMixin:
|
||||
super().__init__()
|
||||
self.add_mixin(PluginMixinEnum.REPORT, True, __class__)
|
||||
|
||||
def add_report_context(self, report_instance, model_instance, request, context):
|
||||
def add_report_context(self, report_instance, model_instance, user, context):
|
||||
"""Add extra context to the provided report instance.
|
||||
|
||||
By default, this method does nothing.
|
||||
@@ -32,11 +32,11 @@ class ReportMixin:
|
||||
Args:
|
||||
report_instance: The report instance to add context to
|
||||
model_instance: The model instance which initiated the report generation
|
||||
request: The request object which initiated the report generation
|
||||
user: The user to associate with the generated report
|
||||
context: The context dictionary to add to
|
||||
"""
|
||||
|
||||
def add_label_context(self, label_instance, model_instance, request, context):
|
||||
def add_label_context(self, label_instance, model_instance, user, context):
|
||||
"""Add extra context to the provided label instance.
|
||||
|
||||
By default, this method does nothing.
|
||||
@@ -44,18 +44,18 @@ class ReportMixin:
|
||||
Args:
|
||||
label_instance: The label instance to add context to
|
||||
model_instance: The model instance which initiated the label generation
|
||||
request: The request object which initiated the label generation
|
||||
user: The user to associate with the generated label
|
||||
context: The context dictionary to add to
|
||||
"""
|
||||
|
||||
def report_callback(self, template, instance, report, request, **kwargs):
|
||||
def report_callback(self, template, instance, report, user, **kwargs):
|
||||
"""Callback function called after a report is generated.
|
||||
|
||||
Arguments:
|
||||
template: The ReportTemplate model
|
||||
instance: The instance of the target model
|
||||
report: The generated report object
|
||||
request: The initiating request object
|
||||
user: The user to associate with the generated report
|
||||
|
||||
The default implementation does nothing.
|
||||
"""
|
||||
|
||||
@@ -38,41 +38,51 @@ class LabelPrintingMixin:
|
||||
|
||||
BLOCKING_PRINT = True
|
||||
|
||||
def render_to_pdf(self, label: LabelTemplate, instance, request, **kwargs):
|
||||
def render_to_pdf(
|
||||
self, label: LabelTemplate, instance, request, user=None, **kwargs
|
||||
):
|
||||
"""Render this label to PDF format.
|
||||
|
||||
Arguments:
|
||||
label: The LabelTemplate object to render against
|
||||
instance: The model instance to render
|
||||
request: The HTTP request object which triggered this print job
|
||||
request: The HTTP request object which triggered this print job (optional, may be None)
|
||||
user: The user who triggered this print job (optional, may be None)
|
||||
"""
|
||||
try:
|
||||
return label.render(instance, request)
|
||||
return label.render(instance, request=request, user=user)
|
||||
except Exception:
|
||||
log_error('render_to_pdf', plugin=self.slug)
|
||||
raise ValidationError(_('Error rendering label to PDF'))
|
||||
|
||||
def render_to_html(self, label: LabelTemplate, instance, request, **kwargs):
|
||||
def render_to_html(
|
||||
self, label: LabelTemplate, instance, request, user=None, **kwargs
|
||||
):
|
||||
"""Render this label to HTML format.
|
||||
|
||||
Arguments:
|
||||
label: The LabelTemplate object to render against
|
||||
instance: The model instance to render
|
||||
request: The HTTP request object which triggered this print job
|
||||
request: The HTTP request object which triggered this print job (optional, may be None)
|
||||
user: The user who triggered this print job (optional, may be None)
|
||||
"""
|
||||
try:
|
||||
return label.render_as_string(instance, request)
|
||||
return label.render_as_string(instance, request=request, user=user)
|
||||
except Exception:
|
||||
log_error('render_to_html', plugin=self.slug)
|
||||
raise ValidationError(_('Error rendering label to HTML'))
|
||||
|
||||
def render_to_png(self, label: LabelTemplate, instance, request=None, **kwargs):
|
||||
def render_to_png(
|
||||
self, label: LabelTemplate, instance, request=None, user=None, **kwargs
|
||||
):
|
||||
"""Render this label to PNG format.
|
||||
|
||||
Arguments:
|
||||
label: The LabelTemplate object to render against
|
||||
instance: The model instance to render
|
||||
request: The HTTP request object which triggered this print job
|
||||
request: The HTTP request object which triggered this print job (optional, may be None)
|
||||
user: The user who triggered this print job (optional, may be None)
|
||||
|
||||
Keyword Arguments:
|
||||
pdf_data: The raw PDF data of the rendered label (if already rendered)
|
||||
dpi: The DPI to use for the PNG rendering
|
||||
@@ -85,7 +95,7 @@ class LabelPrintingMixin:
|
||||
pdf_data = kwargs.get('pdf_data')
|
||||
|
||||
if not pdf_data:
|
||||
pdf_data = self.render_to_pdf(label, instance, request, **kwargs)
|
||||
pdf_data = self.render_to_pdf(label, instance, request, user=user, **kwargs)
|
||||
|
||||
pdf2image_kwargs = {
|
||||
'dpi': kwargs.get('dpi', InvenTreeSetting.get_setting('LABEL_DPI', 300)),
|
||||
@@ -128,10 +138,12 @@ class LabelPrintingMixin:
|
||||
The default implementation simply calls print_label() for each label, producing multiple single label output "jobs"
|
||||
but this can be overridden by the particular plugin.
|
||||
"""
|
||||
try:
|
||||
user = request.user
|
||||
except AttributeError:
|
||||
user = None
|
||||
# Extract user information, in decreasing order of preference
|
||||
user = (
|
||||
kwargs.pop('user', None)
|
||||
or getattr(request, 'user', None)
|
||||
or getattr(output, 'user', None)
|
||||
)
|
||||
|
||||
# Initial state for the output print job
|
||||
output.progress = 0
|
||||
@@ -145,11 +157,11 @@ class LabelPrintingMixin:
|
||||
|
||||
# Generate a label output for each provided item
|
||||
for item in items:
|
||||
context = label.get_context(item, request)
|
||||
context = label.get_context(item, request, user=user)
|
||||
filename = label.generate_filename(context)
|
||||
pdf_data = self.render_to_pdf(label, item, request, **kwargs)
|
||||
pdf_data = self.render_to_pdf(label, item, request, user=user, **kwargs)
|
||||
png_file = self.render_to_png(
|
||||
label, item, request, pdf_data=pdf_data, **kwargs
|
||||
label, item, request, pdf_data=pdf_data, user=user, **kwargs
|
||||
)
|
||||
|
||||
print_args = {
|
||||
|
||||
@@ -20,6 +20,7 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""
|
||||
|
||||
NAME = 'InvenTreeLabel'
|
||||
SLUG = 'inventreelabel'
|
||||
TITLE = _('InvenTree PDF label printer')
|
||||
DESCRIPTION = _('Provides native support for printing PDF labels')
|
||||
VERSION = '1.1.0'
|
||||
|
||||
@@ -215,6 +215,7 @@ class LabelPrint(GenericAPIView):
|
||||
template.pk,
|
||||
[item.pk for item in items_to_print],
|
||||
output.pk,
|
||||
user.pk if user else None,
|
||||
plugin.slug,
|
||||
options=(plugin_serializer.data if plugin_serializer else {}),
|
||||
)
|
||||
@@ -297,7 +298,13 @@ class ReportPrint(GenericAPIView):
|
||||
item_ids = [item.pk for item in items_to_print]
|
||||
|
||||
# Offload the task to the background worker
|
||||
offload_task(report.tasks.print_reports, template.pk, item_ids, output.pk)
|
||||
offload_task(
|
||||
report.tasks.print_reports,
|
||||
template.pk,
|
||||
item_ids,
|
||||
output.pk,
|
||||
user.pk if user else None,
|
||||
)
|
||||
|
||||
output.refresh_from_db()
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class BaseContextExtension(TypedDict):
|
||||
template_description: Description of the report template
|
||||
template_name: Name of the report template
|
||||
template_revision: Revision of the report template
|
||||
user: User who made the request to render the template
|
||||
user: User who is creating the report (if available)
|
||||
"""
|
||||
|
||||
base_url: str
|
||||
@@ -244,37 +244,39 @@ class ReportTemplateBase(
|
||||
def generate_filename(self, context, **kwargs) -> str:
|
||||
"""Generate a filename for this report."""
|
||||
template_string = Template(self.filename_pattern)
|
||||
|
||||
return template_string.render(Context(context))
|
||||
|
||||
def render_as_string(self, instance, request=None, context=None, **kwargs) -> str:
|
||||
def render_as_string(
|
||||
self, instance: models.Model, context: Optional[dict] = None, **kwargs
|
||||
) -> str:
|
||||
"""Render the report to a HTML string.
|
||||
|
||||
Arguments:
|
||||
instance: The model instance to render against
|
||||
request: A HTTPRequest object (optional)
|
||||
context: Django template language contexts (optional)
|
||||
|
||||
Returns:
|
||||
str: HTML string
|
||||
"""
|
||||
if context is None:
|
||||
context = self.get_context(instance, request, **kwargs)
|
||||
context = self.get_context(instance, **kwargs)
|
||||
|
||||
return render_to_string(self.template_name, context, request)
|
||||
return render_to_string(self.template_name, context)
|
||||
|
||||
def render(self, instance, request=None, context=None, **kwargs) -> bytes:
|
||||
def render(
|
||||
self, instance: models.Model, context: Optional[dict] = None, **kwargs
|
||||
) -> bytes:
|
||||
"""Render the template to a PDF file.
|
||||
|
||||
Arguments:
|
||||
instance: The model instance to render against
|
||||
request: A HTTPRequest object (optional)
|
||||
context: Django template langaguage contexts (optional)
|
||||
context: Django template language contexts (optional)
|
||||
user: The user to associate with the generated report
|
||||
|
||||
Returns:
|
||||
bytes: PDF data
|
||||
"""
|
||||
html = self.render_as_string(instance, request, context, **kwargs)
|
||||
html = self.render_as_string(instance, context=context, **kwargs)
|
||||
pdf = HTML(string=html).write_pdf(pdf_forms=True)
|
||||
|
||||
return pdf
|
||||
@@ -323,28 +325,28 @@ class ReportTemplateBase(
|
||||
"""Return a filter dict which can be applied to the target model."""
|
||||
return report.validators.validate_filters(self.filters, model=self.get_model())
|
||||
|
||||
def base_context(self, request=None) -> BaseContextExtension:
|
||||
def base_context(self, **kwargs) -> BaseContextExtension:
|
||||
"""Return base context data (available to all templates)."""
|
||||
return {
|
||||
'base_url': get_base_url(request=request),
|
||||
'base_url': get_base_url(),
|
||||
'date': InvenTree.helpers.current_date(),
|
||||
'datetime': InvenTree.helpers.current_time(),
|
||||
'template': self,
|
||||
'template_description': self.description,
|
||||
'template_name': self.name,
|
||||
'template_revision': self.revision,
|
||||
'user': request.user if request else None,
|
||||
'user': kwargs.get('user'),
|
||||
}
|
||||
|
||||
def get_context(self, instance, request=None, **kwargs):
|
||||
def get_context(self, instance: models.Model, **kwargs):
|
||||
"""Supply context data to the generic template for rendering.
|
||||
|
||||
Arguments:
|
||||
instance: The model instance we are printing against
|
||||
request: The request object (optional)
|
||||
user: The user to associate with the generated report
|
||||
"""
|
||||
# Provide base context information to all templates
|
||||
base_context = self.base_context(request=request)
|
||||
base_context = self.base_context(**kwargs)
|
||||
|
||||
# Add in an context information provided by the model instance itself
|
||||
context = {**base_context, **instance.report_context()}
|
||||
@@ -423,55 +425,77 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
|
||||
return report_context
|
||||
|
||||
def get_context(self, instance, request=None, **kwargs):
|
||||
"""Supply context data to the report template for rendering."""
|
||||
base_context = super().get_context(instance, request)
|
||||
def get_context(self, instance: models.Model, **kwargs):
|
||||
"""Supply context data to the report template for rendering.
|
||||
|
||||
Arguments:
|
||||
instance: The model instance we are printing against
|
||||
"""
|
||||
base_context = super().get_context(instance, **kwargs)
|
||||
report_context: ReportContextExtension = self.get_report_context()
|
||||
|
||||
context = {**base_context, **report_context}
|
||||
|
||||
# Pass the context through to the plugin registry for any additional information
|
||||
context = self.get_plugin_context(instance, request, context)
|
||||
context = self.get_plugin_context(instance, context, **kwargs)
|
||||
return context
|
||||
|
||||
def get_plugin_context(self, instance, request, context):
|
||||
"""Get the context for the plugin."""
|
||||
def get_plugin_context(self, instance: models.Model, context: dict, **kwargs):
|
||||
"""Get the context for the plugin.
|
||||
|
||||
Arguments:
|
||||
instance: The model instance we are printing against
|
||||
context: The context dictionary to add to
|
||||
user: The user to associate with the generated report
|
||||
"""
|
||||
user = kwargs.get('user')
|
||||
|
||||
for plugin in registry.with_mixin(PluginMixinEnum.REPORT):
|
||||
try:
|
||||
plugin.add_report_context(self, instance, request, context)
|
||||
plugin.add_report_context(self, instance, user, context)
|
||||
except Exception:
|
||||
InvenTree.exceptions.log_error('add_report_context', plugin=plugin.slug)
|
||||
|
||||
return context
|
||||
|
||||
def handle_attachment(self, instance, report, report_name, request, debug_mode):
|
||||
def handle_attachment(self, instance, report, report_name, user, debug_mode):
|
||||
"""Attach the generated report to the model instance (if required)."""
|
||||
if self.attach_to_model and not debug_mode:
|
||||
instance.create_attachment(
|
||||
attachment=ContentFile(report, report_name),
|
||||
comment=_(f'Report generated from template {self.name}'),
|
||||
upload_user=request.user
|
||||
if request and request.user.is_authenticated
|
||||
else None,
|
||||
upload_user=user,
|
||||
)
|
||||
|
||||
def notify_plugins(self, instance, report, request):
|
||||
"""Provide generated report to any interested plugins."""
|
||||
def notify_plugins(self, instance, report, user):
|
||||
"""Provide generated report to any interested plugins.
|
||||
|
||||
Arguments:
|
||||
instance: The model instance we are printing against
|
||||
report: The generated report object
|
||||
user: The user to associate with the generated report
|
||||
"""
|
||||
report_plugins = registry.with_mixin(PluginMixinEnum.REPORT)
|
||||
|
||||
for plugin in report_plugins:
|
||||
try:
|
||||
plugin.report_callback(self, instance, report, request)
|
||||
plugin.report_callback(self, instance, report, user)
|
||||
except Exception:
|
||||
InvenTree.exceptions.log_error('report_callback', plugin=plugin.slug)
|
||||
|
||||
def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput:
|
||||
def print(
|
||||
self,
|
||||
items: list,
|
||||
output: Optional[DataOutput] = None,
|
||||
user: Optional[AbstractUser] = None,
|
||||
**kwargs,
|
||||
) -> DataOutput:
|
||||
"""Print reports for a list of items against this template.
|
||||
|
||||
Arguments:
|
||||
items: A list of items to print reports for (model instance)
|
||||
output: The DataOutput object to use (if provided)
|
||||
request: The request object (optional)
|
||||
output: The DataOutput object to use
|
||||
user: The user to associate with the generated report
|
||||
|
||||
Returns:
|
||||
output: The DataOutput object representing the generated report(s)
|
||||
@@ -489,6 +513,9 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
"""
|
||||
logger.info("Printing %s reports against template '%s'", len(items), self.name)
|
||||
|
||||
# Extract user information from the provided context
|
||||
user = user or getattr(output, 'user', None)
|
||||
|
||||
outputs = []
|
||||
|
||||
debug_mode = get_global_setting('REPORT_DEBUG_MODE', False)
|
||||
@@ -500,9 +527,7 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
if not output:
|
||||
output = DataOutput.objects.create(
|
||||
total=len(items),
|
||||
user=request.user
|
||||
if request and request.user and request.user.is_authenticated
|
||||
else None,
|
||||
user=user,
|
||||
progress=0,
|
||||
complete=False,
|
||||
output_type=DataOutput.DataOutputTypes.REPORT,
|
||||
@@ -516,13 +541,13 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
|
||||
try:
|
||||
if self.merge:
|
||||
base_context = super().base_context(request)
|
||||
base_context = super().base_context(user=user)
|
||||
report_context = self.get_report_context()
|
||||
item_contexts = []
|
||||
for instance in items:
|
||||
instance_context = instance.report_context()
|
||||
instance_context = self.get_plugin_context(
|
||||
instance, request, instance_context
|
||||
instance, instance_context, user=user
|
||||
)
|
||||
item_contexts.append(instance_context)
|
||||
|
||||
@@ -537,9 +562,11 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
|
||||
try:
|
||||
if debug_mode:
|
||||
report = self.render_as_string(instance, request, contexts)
|
||||
report = self.render_as_string(
|
||||
instance, user=user, context=contexts
|
||||
)
|
||||
else:
|
||||
report = self.render(instance, request, contexts)
|
||||
report = self.render(instance, user=user, context=contexts)
|
||||
except TemplateDoesNotExist as e:
|
||||
t_name = str(e) or self.template
|
||||
msg = f'Template file {t_name} does not exist'
|
||||
@@ -558,17 +585,15 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
raise ValidationError(f'{msg}: {e!s}')
|
||||
|
||||
outputs.append(report)
|
||||
self.handle_attachment(
|
||||
instance, report, report_name, request, debug_mode
|
||||
)
|
||||
self.notify_plugins(instance, report, request)
|
||||
self.handle_attachment(instance, report, report_name, user, debug_mode)
|
||||
self.notify_plugins(instance, report, user)
|
||||
|
||||
# Update the progress of the report generation
|
||||
output.progress += 1
|
||||
output.save()
|
||||
else:
|
||||
for instance in items:
|
||||
context = self.get_context(instance, request)
|
||||
context = self.get_context(instance, user=user)
|
||||
|
||||
if report_name is None:
|
||||
report_name = self.generate_filename(context)
|
||||
@@ -576,9 +601,11 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
# Render the report output
|
||||
try:
|
||||
if debug_mode:
|
||||
report = self.render_as_string(instance, request, None)
|
||||
report = self.render_as_string(
|
||||
instance, user=user, context=context
|
||||
)
|
||||
else:
|
||||
report = self.render(instance, request, None)
|
||||
report = self.render(instance, user=user, context=None)
|
||||
except TemplateDoesNotExist as e:
|
||||
t_name = str(e) or self.template
|
||||
msg = f'Template file {t_name} does not exist'
|
||||
@@ -599,9 +626,10 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
outputs.append(report)
|
||||
|
||||
self.handle_attachment(
|
||||
instance, report, report_name, request, debug_mode
|
||||
instance, report, report_name, user, debug_mode
|
||||
)
|
||||
self.notify_plugins(instance, report, request)
|
||||
|
||||
self.notify_plugins(instance, report, user)
|
||||
|
||||
# Update the progress of the report generation
|
||||
output.progress += 1
|
||||
@@ -614,7 +642,6 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
raise ValidationError({
|
||||
'error': _('Error generating report'),
|
||||
'detail': str(exc),
|
||||
'path': request.path if request else None,
|
||||
})
|
||||
|
||||
if not report_name:
|
||||
@@ -704,9 +731,16 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
}}
|
||||
"""
|
||||
|
||||
def get_context(self, instance, request=None, **kwargs):
|
||||
"""Supply context data to the label template for rendering."""
|
||||
base_context = super().get_context(instance, request, **kwargs)
|
||||
def get_context(self, instance: models.Model, *args, **kwargs):
|
||||
"""Supply context data to the label template for rendering.
|
||||
|
||||
Arguments:
|
||||
instance: The model instance we are printing against
|
||||
"""
|
||||
user = kwargs.get('user')
|
||||
|
||||
base_context = super().get_context(instance, **kwargs)
|
||||
|
||||
label_context: LabelContextExtension = {
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
@@ -724,7 +758,7 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
for plugin in plugins:
|
||||
# Let each plugin add its own context data
|
||||
try:
|
||||
plugin.add_label_context(self, instance, request, context)
|
||||
plugin.add_label_context(self, instance, user, context)
|
||||
except Exception:
|
||||
InvenTree.exceptions.log_error('add_label_context', plugin=plugin.slug)
|
||||
|
||||
@@ -734,9 +768,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
self,
|
||||
items: list,
|
||||
plugin: InvenTreePlugin,
|
||||
output=None,
|
||||
options=None,
|
||||
request=None,
|
||||
output: Optional[DataOutput] = None,
|
||||
options: Optional[dict] = None,
|
||||
user: Optional[AbstractUser] = None,
|
||||
**kwargs,
|
||||
) -> DataOutput:
|
||||
"""Print labels for a list of items against this template.
|
||||
@@ -744,9 +778,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
Arguments:
|
||||
items: A list of items to print labels for (model instance)
|
||||
plugin: The plugin to use for label rendering
|
||||
output: The DataOutput object to use (if provided)
|
||||
output: The DataOutput object to use
|
||||
options: Additional options for the label printing plugin (optional)
|
||||
request: The request object (optional)
|
||||
user: The user to associate with the generated labels
|
||||
|
||||
Returns:
|
||||
output: The DataOutput object representing the generated label(s)
|
||||
@@ -758,11 +792,11 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
f"Printing {len(items)} labels against template '{self.name}' using plugin '{plugin.slug}'"
|
||||
)
|
||||
|
||||
user = user or getattr(output, 'user', None)
|
||||
|
||||
if not output:
|
||||
output = DataOutput.objects.create(
|
||||
user=request.user
|
||||
if request and request.user.is_authenticated
|
||||
else None,
|
||||
user=user,
|
||||
total=len(items),
|
||||
progress=0,
|
||||
complete=False,
|
||||
@@ -779,7 +813,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
if hasattr(plugin, 'before_printing'):
|
||||
plugin.before_printing()
|
||||
|
||||
plugin.print_labels(self, output, items, request, printing_options=options)
|
||||
plugin.print_labels(
|
||||
self, output, items, None, user=user, printing_options=options
|
||||
)
|
||||
|
||||
if hasattr(plugin, 'after_printing'):
|
||||
plugin.after_printing()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Background tasks for the report app."""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
import structlog
|
||||
from opentelemetry import trace
|
||||
|
||||
@@ -10,13 +12,16 @@ logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
@tracer.start_as_current_span('print_reports')
|
||||
def print_reports(template_id: int, item_ids: list[int], output_id: int, **kwargs):
|
||||
def print_reports(
|
||||
template_id: int, item_ids: list[int], output_id: int, user_id: int, **kwargs
|
||||
):
|
||||
"""Print multiple reports against the provided template.
|
||||
|
||||
Arguments:
|
||||
template_id: The ID of the ReportTemplate to use
|
||||
item_ids: List of item IDs to generate the report against
|
||||
output_id: The ID of the DataOutput to use (if provided)
|
||||
output_id: The ID of the DataOutput to use
|
||||
user_id: The ID of the user to associate with the generated report
|
||||
|
||||
This function is intended to be called by the background worker,
|
||||
and will continuously update the status of the DataOutput object.
|
||||
@@ -31,6 +36,18 @@ def print_reports(template_id: int, item_ids: list[int], output_id: int, **kwarg
|
||||
log_error('report.tasks.print_reports')
|
||||
return
|
||||
|
||||
# Fetch user information
|
||||
user = None
|
||||
|
||||
if user_id:
|
||||
try:
|
||||
user = get_user_model().objects.get(pk=user_id)
|
||||
except Exception:
|
||||
log_error('report.tasks.print_reports', user_id=user_id)
|
||||
|
||||
if not user:
|
||||
user = getattr(output, 'user', None)
|
||||
|
||||
# Fetch the items to be included in the report
|
||||
model = template.get_model()
|
||||
items = model.objects.filter(pk__in=item_ids)
|
||||
@@ -38,20 +55,26 @@ def print_reports(template_id: int, item_ids: list[int], output_id: int, **kwarg
|
||||
# Ensure they are sorted by the order of the provided item IDs
|
||||
items = sorted(items, key=lambda item: item_ids.index(item.pk))
|
||||
|
||||
template.print(items, output=output)
|
||||
template.print(items, output=output, user=user)
|
||||
|
||||
|
||||
@tracer.start_as_current_span('print_labels')
|
||||
def print_labels(
|
||||
template_id: int, item_ids: list[int], output_id: int, plugin_slug: str, **kwargs
|
||||
template_id: int,
|
||||
item_ids: list[int],
|
||||
output_id: int,
|
||||
user_id: int,
|
||||
plugin_slug: str,
|
||||
**kwargs,
|
||||
):
|
||||
"""Print multiple labels against the provided template.
|
||||
|
||||
Arguments:
|
||||
template_id: The ID of the LabelTemplate to use
|
||||
item_ids: List of item IDs to generate the labels against
|
||||
output_id: The ID of the DataOutput to use (if provided)
|
||||
plugin_slug: The ID of the LabelPlugin to use (if provided)
|
||||
output_id: The ID of the DataOutput to use
|
||||
user_id: The ID of the user to associate with the generated labels
|
||||
plugin_slug: The ID of the LabelPlugin to use
|
||||
|
||||
This function is intended to be called by the background worker,
|
||||
and will continuously update the status of the DataOutput object.
|
||||
@@ -67,6 +90,18 @@ def print_labels(
|
||||
log_error('report.tasks.print_labels')
|
||||
return
|
||||
|
||||
# Fetch user information
|
||||
user = None
|
||||
|
||||
if user_id:
|
||||
try:
|
||||
user = get_user_model().objects.get(pk=user_id)
|
||||
except Exception:
|
||||
log_error('report.tasks.print_labels', user_id=user_id)
|
||||
|
||||
if not user:
|
||||
user = getattr(output, 'user', None)
|
||||
|
||||
# Fetch the items to be included in the report
|
||||
model = template.get_model()
|
||||
items = model.objects.filter(pk__in=item_ids)
|
||||
@@ -83,4 +118,4 @@ def print_labels(
|
||||
# Extract optional arguments for label printing
|
||||
options = kwargs.pop('options') or {}
|
||||
|
||||
template.print(items, plugin, output=output, options=options)
|
||||
template.print(items, plugin, output=output, user=user, options=options)
|
||||
|
||||
@@ -6,8 +6,12 @@ from io import StringIO
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.urls import reverse
|
||||
|
||||
from pypdf import PdfReader
|
||||
|
||||
import report.models as report_models
|
||||
from build.models import Build
|
||||
from common.models import Attachment
|
||||
@@ -299,6 +303,92 @@ class ReportTest(InvenTreeAPITestCase):
|
||||
self.assertIsNotNone(output.output)
|
||||
self.assertTrue(output.output.name.endswith('.pdf'))
|
||||
|
||||
def test_print_custom_template(self):
|
||||
"""Create a new template, print it, and check the output."""
|
||||
template_string = """
|
||||
Hello {{ user.username }}
|
||||
Your user ID is {{ user.pk }}.
|
||||
Template name: {{ template.name }}
|
||||
{% if merge %}
|
||||
REPORT OUTPUT: MERGE = ENABLED
|
||||
{% for instance in instances %}
|
||||
Part Name: {{ instance.part.name }}
|
||||
Stock ID: {{ instance.stock_item.pk }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
REPORT OUTPUT: MERGE = DISABLED
|
||||
Part Name: {{ part.name }}
|
||||
Stock ID: {{ stock_item.pk }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
template_file = ContentFile(
|
||||
template_string.encode('utf-8'), name='TestPrintTemplate.html'
|
||||
)
|
||||
|
||||
# Create a new report template with the above string as the template
|
||||
template = ReportTemplate.objects.create(
|
||||
name='Test report template',
|
||||
model_type='stockitem',
|
||||
template=template_file,
|
||||
filename_pattern='unit_test_report.pdf',
|
||||
)
|
||||
|
||||
item = StockItem.objects.first()
|
||||
|
||||
test_strings = [
|
||||
f'Hello {self.user.username}',
|
||||
f'Your user ID is {self.user.pk}.',
|
||||
f'Template name: {template.name}',
|
||||
f'Part Name: {item.part.name}',
|
||||
f'Stock ID: {item.pk}',
|
||||
]
|
||||
|
||||
url = reverse('api-report-print')
|
||||
post_data = {'template': template.pk, 'items': [item.pk]}
|
||||
|
||||
# Test with "debug" both enabled and disabled
|
||||
for debug in [True, False]:
|
||||
set_global_setting('REPORT_DEBUG_MODE', debug)
|
||||
|
||||
# Test with "merge" both enabled and disabled
|
||||
for merge in [True, False]:
|
||||
template.merge = merge
|
||||
template.save()
|
||||
|
||||
# Generate report via the API
|
||||
data = self.post(url, data=post_data).data
|
||||
|
||||
self.assertEqual(data['user'], self.user.pk)
|
||||
self.assertIsNotNone(data['output'])
|
||||
self.assertTrue(data['output'].endswith('.html' if debug else '.pdf'))
|
||||
self.assertIn('unit_test_report', data['output'])
|
||||
|
||||
if debug:
|
||||
# Read raw HTML file
|
||||
output = default_storage.open(
|
||||
data['output'].replace('/media/', '', 1)
|
||||
)
|
||||
file_content = str(output.read(), 'utf-8')
|
||||
else:
|
||||
# Convert from PDF bytes to string for testing purposes
|
||||
output_path = os.path.join(
|
||||
settings.MEDIA_ROOT, data['output'].replace('/media/', '', 1)
|
||||
)
|
||||
reader = PdfReader(output_path)
|
||||
file_content = ''.join(page.extract_text() for page in reader.pages)
|
||||
|
||||
# Replace any newline characters for testing purposes
|
||||
file_content = file_content.replace('\n', ' ')
|
||||
|
||||
for ts in test_strings:
|
||||
self.assertIn(ts, file_content)
|
||||
|
||||
self.assertIn(
|
||||
f'REPORT OUTPUT: MERGE = {"ENABLED" if merge else "DISABLED"}',
|
||||
file_content,
|
||||
)
|
||||
|
||||
|
||||
class LabelTest(InvenTreeAPITestCase):
|
||||
"""Unit tests for label templates."""
|
||||
@@ -351,6 +441,71 @@ class LabelTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(output.plugin, 'inventreelabel')
|
||||
self.assertTrue(output.output.name.endswith('.pdf'))
|
||||
|
||||
def test_print_custom_template(self):
|
||||
"""Test printing against a custom template file."""
|
||||
template_string = """
|
||||
Hello {{ user.username }} - your user ID is {{ user.pk }}.
|
||||
Template name: {{ template.name }}
|
||||
Barcode: {{ qr_data }}
|
||||
Location ID: {{ location.pk }}
|
||||
Base URL: {{ base_url }}
|
||||
"""
|
||||
|
||||
template_file = ContentFile(
|
||||
template_string.encode('utf-8'), name='TestLabelTemplate.html'
|
||||
)
|
||||
|
||||
# Create a new label template with the above string as the template
|
||||
template = LabelTemplate.objects.create(
|
||||
name='Test label template',
|
||||
model_type='stocklocation',
|
||||
template=template_file,
|
||||
filename_pattern='unit_test_label.pdf',
|
||||
)
|
||||
|
||||
location = StockItem.objects.exclude(location=None).first().location
|
||||
|
||||
url = reverse('api-label-print')
|
||||
post_data = {'template': template.pk, 'items': [location.pk]}
|
||||
|
||||
plugin = registry.get_plugin('inventreelabel')
|
||||
|
||||
test_strings = [
|
||||
f'Hello {self.user.username} - your user ID is {self.user.pk}.',
|
||||
f'Template name: {template.name}',
|
||||
f'Location ID: {location.pk}',
|
||||
f'INV-SL{location.pk}',
|
||||
]
|
||||
|
||||
# Test with "debug" both enabled and disabled
|
||||
for debug in [True, False]:
|
||||
plugin.set_setting('DEBUG', debug)
|
||||
|
||||
# Generate label via the API
|
||||
data = self.post(url, data=post_data).data
|
||||
|
||||
self.assertEqual(data['user'], self.user.pk)
|
||||
self.assertTrue(data['output'].endswith('.html' if debug else '.pdf'))
|
||||
|
||||
# Read the file contents back out, and validate
|
||||
if debug:
|
||||
# Read raw HTML file
|
||||
output = default_storage.open(data['output'].replace('/media/', '', 1))
|
||||
file_content = str(output.read(), 'utf-8')
|
||||
else:
|
||||
# Convert from PDF bytes to string for testing purposes
|
||||
output_path = os.path.join(
|
||||
settings.MEDIA_ROOT, data['output'].replace('/media/', '', 1)
|
||||
)
|
||||
reader = PdfReader(output_path)
|
||||
file_content = ''.join(page.extract_text() for page in reader.pages)
|
||||
|
||||
# Replace any newline for testing purposes
|
||||
file_content = file_content.replace('\n', ' ')
|
||||
|
||||
for ts in test_strings:
|
||||
self.assertIn(ts, file_content)
|
||||
|
||||
def test_filters(self):
|
||||
"""Test that template filters are correctly validated."""
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
Reference in New Issue
Block a user