mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
Report print update (#9002)
* Refactor label printing - Move "print" method to template - Allows for internal calls to printing (e.g. plugins) * Generate dummy request - Required to trick WeasyPrint * Refactor reportprinting * Add timeout for unit test * More unit tests * Tweak unit test * Updated comment
This commit is contained in:
parent
821b311d73
commit
855afde4e5
@ -124,6 +124,7 @@ class APICallMixin:
|
|||||||
headers: Optional[dict] = None,
|
headers: Optional[dict] = None,
|
||||||
simple_response: bool = True,
|
simple_response: bool = True,
|
||||||
endpoint_is_url: bool = False,
|
endpoint_is_url: bool = False,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Do an API call.
|
"""Do an API call.
|
||||||
|
|
||||||
@ -161,7 +162,7 @@ class APICallMixin:
|
|||||||
url = f'{self.api_url}/{endpoint}'
|
url = f'{self.api_url}/{endpoint}'
|
||||||
|
|
||||||
# build kwargs for call
|
# build kwargs for call
|
||||||
kwargs = {'url': url, 'headers': headers}
|
kwargs.update({'url': url, 'headers': headers})
|
||||||
|
|
||||||
if data and json:
|
if data and json:
|
||||||
raise ValueError('You can either pass `data` or `json` to this function.')
|
raise ValueError('You can either pass `data` or `json` to this function.')
|
||||||
|
@ -46,7 +46,7 @@ class ReportMixin:
|
|||||||
context: The context dictionary to add to
|
context: The context dictionary to add to
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def report_callback(self, template, instance, report, request):
|
def report_callback(self, template, instance, report, request, **kwargs):
|
||||||
"""Callback function called after a report is generated.
|
"""Callback function called after a report is generated.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
@ -291,6 +291,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
|
|||||||
json={'name': 'morpheus', 'job': 'leader'},
|
json={'name': 'morpheus', 'job': 'leader'},
|
||||||
method='POST',
|
method='POST',
|
||||||
endpoint_is_url=True,
|
endpoint_is_url=True,
|
||||||
|
timeout=5000,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
"""API functionality for the 'report' app."""
|
"""API functionality for the 'report' app."""
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
from django.template.exceptions import TemplateDoesNotExist
|
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -14,7 +12,6 @@ from rest_framework import permissions
|
|||||||
from rest_framework.generics import GenericAPIView
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
import common.models
|
|
||||||
import InvenTree.exceptions
|
import InvenTree.exceptions
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.permissions
|
import InvenTree.permissions
|
||||||
@ -22,11 +19,9 @@ import report.helpers
|
|||||||
import report.models
|
import report.models
|
||||||
import report.serializers
|
import report.serializers
|
||||||
from InvenTree.api import BulkDeleteMixin, MetadataView
|
from InvenTree.api import BulkDeleteMixin, MetadataView
|
||||||
from InvenTree.exceptions import log_error
|
|
||||||
from InvenTree.filters import InvenTreeSearchFilter
|
from InvenTree.filters import InvenTreeSearchFilter
|
||||||
from InvenTree.mixins import ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
|
from InvenTree.mixins import ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
|
||||||
from plugin.builtin.labels.inventree_label import InvenTreeLabelPlugin
|
from plugin.builtin.labels.inventree_label import InvenTreeLabelPlugin
|
||||||
from plugin.registry import registry
|
|
||||||
|
|
||||||
|
|
||||||
class TemplatePermissionMixin:
|
class TemplatePermissionMixin:
|
||||||
@ -204,32 +199,13 @@ class LabelPrint(GenericAPIView):
|
|||||||
):
|
):
|
||||||
plugin_serializer.is_valid(raise_exception=True)
|
plugin_serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
# Create a new LabelOutput instance to print against
|
output = template.print(
|
||||||
output = report.models.LabelOutput.objects.create(
|
items_to_print,
|
||||||
template=template,
|
plugin,
|
||||||
items=len(items_to_print),
|
options=(plugin_serializer.data if plugin_serializer else {}),
|
||||||
plugin=plugin.slug,
|
request=request,
|
||||||
user=request.user,
|
|
||||||
progress=0,
|
|
||||||
complete=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
plugin.before_printing()
|
|
||||||
plugin.print_labels(
|
|
||||||
template,
|
|
||||||
output,
|
|
||||||
items_to_print,
|
|
||||||
request,
|
|
||||||
printing_options=(plugin_serializer.data if plugin_serializer else {}),
|
|
||||||
)
|
|
||||||
plugin.after_printing()
|
|
||||||
except ValidationError as e:
|
|
||||||
raise (e)
|
|
||||||
except Exception as e:
|
|
||||||
InvenTree.exceptions.log_error(f'plugins.{plugin.slug}.print_labels')
|
|
||||||
raise ValidationError([_('Error printing label'), str(e)])
|
|
||||||
|
|
||||||
output.refresh_from_db()
|
output.refresh_from_db()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
@ -280,124 +256,7 @@ class ReportPrint(GenericAPIView):
|
|||||||
|
|
||||||
def print(self, template, items_to_print, request):
|
def print(self, template, items_to_print, request):
|
||||||
"""Print this report template against a number of provided items."""
|
"""Print this report template against a number of provided items."""
|
||||||
outputs = []
|
output = template.print(items_to_print, request)
|
||||||
|
|
||||||
# In debug mode, generate single HTML output, rather than PDF
|
|
||||||
debug_mode = common.models.InvenTreeSetting.get_setting(
|
|
||||||
'REPORT_DEBUG_MODE', cache=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start with a default report name
|
|
||||||
report_name = 'report.pdf'
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Merge one or more PDF files into a single download
|
|
||||||
for instance in items_to_print:
|
|
||||||
context = template.get_context(instance, request)
|
|
||||||
report_name = template.generate_filename(context)
|
|
||||||
|
|
||||||
output = template.render(instance, request)
|
|
||||||
|
|
||||||
if template.attach_to_model:
|
|
||||||
# Attach the generated report to the model instance
|
|
||||||
data = output.get_document().write_pdf()
|
|
||||||
instance.create_attachment(
|
|
||||||
attachment=ContentFile(data, report_name),
|
|
||||||
comment=_('Report saved at time of printing'),
|
|
||||||
upload_user=request.user,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Provide generated report to any interested plugins
|
|
||||||
for plugin in registry.with_mixin('report'):
|
|
||||||
try:
|
|
||||||
plugin.report_callback(self, instance, output, request)
|
|
||||||
except Exception:
|
|
||||||
InvenTree.exceptions.log_error(
|
|
||||||
f'plugins.{plugin.slug}.report_callback'
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if debug_mode:
|
|
||||||
outputs.append(template.render_as_string(instance, request))
|
|
||||||
else:
|
|
||||||
outputs.append(template.render(instance, request))
|
|
||||||
except TemplateDoesNotExist as e:
|
|
||||||
template = str(e)
|
|
||||||
if not template:
|
|
||||||
template = template.template
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
'error': _(
|
|
||||||
f"Template file '{template}' is missing or does not exist"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not report_name.endswith('.pdf'):
|
|
||||||
report_name += '.pdf'
|
|
||||||
|
|
||||||
if debug_mode:
|
|
||||||
"""Concatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
|
|
||||||
|
|
||||||
data = '\n'.join(outputs)
|
|
||||||
report_name = report_name.replace('.pdf', '.html')
|
|
||||||
else:
|
|
||||||
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
|
|
||||||
|
|
||||||
pages = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
for output in outputs:
|
|
||||||
doc = output.get_document()
|
|
||||||
for page in doc.pages:
|
|
||||||
pages.append(page)
|
|
||||||
|
|
||||||
data = outputs[0].get_document().copy(pages).write_pdf()
|
|
||||||
|
|
||||||
except TemplateDoesNotExist as e:
|
|
||||||
template = str(e)
|
|
||||||
|
|
||||||
if not template:
|
|
||||||
template = template.template
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
'error': _(
|
|
||||||
f"Template file '{template}' is missing or does not exist"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as exc:
|
|
||||||
# Log the exception to the database
|
|
||||||
if InvenTree.helpers.str2bool(
|
|
||||||
common.models.InvenTreeSetting.get_setting(
|
|
||||||
'REPORT_LOG_ERRORS', cache=False
|
|
||||||
)
|
|
||||||
):
|
|
||||||
log_error(request.path)
|
|
||||||
|
|
||||||
# Re-throw the exception to the client as a DRF exception
|
|
||||||
raise ValidationError({
|
|
||||||
'error': 'Report printing failed',
|
|
||||||
'detail': str(exc),
|
|
||||||
'path': request.path,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Generate a report output object
|
|
||||||
# TODO: This should be moved to a separate function
|
|
||||||
# TODO: Allow background printing of reports, with progress reporting
|
|
||||||
output = report.models.ReportOutput.objects.create(
|
|
||||||
template=template,
|
|
||||||
items=len(items_to_print),
|
|
||||||
user=request.user,
|
|
||||||
progress=100,
|
|
||||||
complete=True,
|
|
||||||
output=ContentFile(data, report_name),
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
report.serializers.ReportOutputSerializer(output).data, status=201
|
report.serializers.ReportOutputSerializer(output).data, status=201
|
||||||
|
@ -5,13 +5,17 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import AnonymousUser, User
|
||||||
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.validators import FileExtensionValidator, MinValueValidator
|
from django.core.validators import FileExtensionValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.http import HttpRequest
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
|
from django.template.exceptions import TemplateDoesNotExist
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
from django.test.client import RequestFactory
|
||||||
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 _
|
||||||
|
|
||||||
@ -23,6 +27,7 @@ import report.validators
|
|||||||
from common.settings import get_global_setting
|
from common.settings import get_global_setting
|
||||||
from InvenTree.helpers_model import get_base_url
|
from InvenTree.helpers_model import get_base_url
|
||||||
from InvenTree.models import MetadataMixin
|
from InvenTree.models import MetadataMixin
|
||||||
|
from plugin import InvenTreePlugin
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -37,6 +42,17 @@ except OSError as err: # pragma: no cover
|
|||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
|
def dummy_print_request() -> HttpRequest:
|
||||||
|
"""Generate a dummy HTTP request object.
|
||||||
|
|
||||||
|
This is required for internal print calls, as WeasyPrint *requires* a request object.
|
||||||
|
"""
|
||||||
|
factory = RequestFactory()
|
||||||
|
request = factory.get('/')
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
return request
|
||||||
|
|
||||||
|
|
||||||
class WeasyprintReport(WeasyTemplateResponseMixin):
|
class WeasyprintReport(WeasyTemplateResponseMixin):
|
||||||
"""Class for rendering a HTML template to a PDF."""
|
"""Class for rendering a HTML template to a PDF."""
|
||||||
|
|
||||||
@ -67,7 +83,7 @@ def rename_template(instance, filename):
|
|||||||
|
|
||||||
|
|
||||||
class TemplateUploadMixin:
|
class TemplateUploadMixin:
|
||||||
"""Mixin class for providing template pathing functions.
|
"""Mixin class for providing template path management functions.
|
||||||
|
|
||||||
- Provides generic method for determining the upload path for a template
|
- Provides generic method for determining the upload path for a template
|
||||||
- Provides generic method for checking for duplicate filenames
|
- Provides generic method for checking for duplicate filenames
|
||||||
@ -197,7 +213,7 @@ class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
|
|||||||
wp = WeasyprintReport(
|
wp = WeasyprintReport(
|
||||||
request,
|
request,
|
||||||
self.template_name,
|
self.template_name,
|
||||||
base_url=request.build_absolute_uri('/'),
|
base_url=get_base_url(request=request),
|
||||||
presentational_hints=True,
|
presentational_hints=True,
|
||||||
filename=self.generate_filename(context),
|
filename=self.generate_filename(context),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -351,6 +367,124 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def print(self, items: list, request=None, **kwargs) -> 'ReportOutput':
|
||||||
|
"""Print reports for a list of items against this template.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
items: A list of items to print reports for (model instance)
|
||||||
|
request: The request object (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
output: The ReportOutput object representing the generated report(s)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If there is an error during report printing
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Currently, all items are rendered separately into PDF files,
|
||||||
|
and then combined into a single PDF file.
|
||||||
|
|
||||||
|
Further work is required to allow the following extended features:
|
||||||
|
- Render a single PDF file with the collated items (optional per template)
|
||||||
|
- Render a raw file (do not convert to PDF) - allows for other file types
|
||||||
|
- Render using background worker, provide progress updates to UI
|
||||||
|
- Run report generation in the background worker process
|
||||||
|
"""
|
||||||
|
outputs = []
|
||||||
|
|
||||||
|
debug_mode = get_global_setting('REPORT_DEBUG_MODE', False)
|
||||||
|
|
||||||
|
# Start with a default report name
|
||||||
|
report_name = None
|
||||||
|
|
||||||
|
if request is None:
|
||||||
|
# Generate a dummy request object
|
||||||
|
request = dummy_print_request()
|
||||||
|
|
||||||
|
report_plugins = registry.with_mixin('report')
|
||||||
|
|
||||||
|
try:
|
||||||
|
for instance in items:
|
||||||
|
context = self.get_context(instance, request)
|
||||||
|
|
||||||
|
if report_name is None:
|
||||||
|
report_name = self.generate_filename(context)
|
||||||
|
|
||||||
|
# Render the report output
|
||||||
|
try:
|
||||||
|
if debug_mode:
|
||||||
|
output = self.render_as_string(instance, request)
|
||||||
|
else:
|
||||||
|
output = self.render(instance, request)
|
||||||
|
except TemplateDoesNotExist as e:
|
||||||
|
t_name = str(e) or self.template
|
||||||
|
raise ValidationError(f'Template file {t_name} does not exist')
|
||||||
|
|
||||||
|
outputs.append(output)
|
||||||
|
|
||||||
|
# Attach the generated report to the model instance (if required)
|
||||||
|
if self.attach_to_model and not debug_mode:
|
||||||
|
data = output.get_document().write_pdf()
|
||||||
|
instance.create_attachment(
|
||||||
|
attachment=ContentFile(data, report_name),
|
||||||
|
comment=_(f'Report generated from template {self.name}'),
|
||||||
|
upload_user=request.user
|
||||||
|
if request.user.is_authenticated
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Provide generated report to any interested plugins
|
||||||
|
for plugin in report_plugins:
|
||||||
|
try:
|
||||||
|
plugin.report_callback(self, instance, output, request)
|
||||||
|
except Exception:
|
||||||
|
InvenTree.exceptions.log_error(
|
||||||
|
f'plugins.{plugin.slug}.report_callback'
|
||||||
|
)
|
||||||
|
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('report.print')
|
||||||
|
|
||||||
|
raise ValidationError({
|
||||||
|
'error': _('Error generating report'),
|
||||||
|
'detail': str(exc),
|
||||||
|
'path': request.path,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not report_name.endswith('.pdf'):
|
||||||
|
report_name += '.pdf'
|
||||||
|
|
||||||
|
# Combine all the generated reports into a single PDF file
|
||||||
|
if debug_mode:
|
||||||
|
data = '\n'.join(outputs)
|
||||||
|
report_name = report_name.replace('.pdf', '.html')
|
||||||
|
else:
|
||||||
|
pages = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for output in outputs:
|
||||||
|
doc = output.get_document()
|
||||||
|
for page in doc.pages:
|
||||||
|
pages.append(page)
|
||||||
|
|
||||||
|
data = outputs[0].get_document().copy(pages).write_pdf()
|
||||||
|
except TemplateDoesNotExist as exc:
|
||||||
|
t_name = str(exc) or self.template
|
||||||
|
raise ValidationError(f'Template file {t_name} does not exist')
|
||||||
|
|
||||||
|
# Save the generated report to the database
|
||||||
|
output = ReportOutput.objects.create(
|
||||||
|
template=self,
|
||||||
|
items=len(items),
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
progress=100,
|
||||||
|
complete=True,
|
||||||
|
output=ContentFile(data, report_name),
|
||||||
|
)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||||
"""Class representing the LabelTemplate database model."""
|
"""Class representing the LabelTemplate database model."""
|
||||||
@ -425,6 +559,64 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def print(
|
||||||
|
self, items: list, plugin: InvenTreePlugin, options=None, request=None, **kwargs
|
||||||
|
) -> 'LabelOutput':
|
||||||
|
"""Print labels for a list of items against this template.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
items: A list of items to print labels for (model instance)
|
||||||
|
plugin: The plugin to use for label rendering
|
||||||
|
options: Additional options for the label printing plugin (optional)
|
||||||
|
request: The request object (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
output: The LabelOutput object representing the generated label(s)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If there is an error during label printing
|
||||||
|
"""
|
||||||
|
output = LabelOutput.objects.create(
|
||||||
|
template=self,
|
||||||
|
items=len(items),
|
||||||
|
plugin=plugin.slug,
|
||||||
|
user=request.user if request else None,
|
||||||
|
progress=0,
|
||||||
|
complete=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if options is None:
|
||||||
|
options = {}
|
||||||
|
|
||||||
|
if request is None:
|
||||||
|
# If the request object is not provided, we need to create a dummy one
|
||||||
|
# Otherwise, WeasyPrint throws an error
|
||||||
|
request = dummy_print_request()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(plugin, 'before_printing'):
|
||||||
|
plugin.before_printing()
|
||||||
|
|
||||||
|
plugin.print_labels(
|
||||||
|
self, output, items, request, printing_options=options
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(plugin, 'after_printing'):
|
||||||
|
plugin.after_printing()
|
||||||
|
except ValidationError as e:
|
||||||
|
output.delete()
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
output.delete()
|
||||||
|
InvenTree.exceptions.log_error(f'plugins.{plugin.slug}.print_labels')
|
||||||
|
raise ValidationError([_('Error printing labels'), str(e)])
|
||||||
|
|
||||||
|
output.complete = True
|
||||||
|
output.save()
|
||||||
|
|
||||||
|
# Return the output object
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
class TemplateOutput(models.Model):
|
class TemplateOutput(models.Model):
|
||||||
"""Base class representing a generated file from a template.
|
"""Base class representing a generated file from a template.
|
||||||
|
@ -170,9 +170,15 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
|||||||
obj = Part.objects.create(name='test', description='test')
|
obj = Part.objects.create(name='test', description='test')
|
||||||
self.assertEqual(report_tags.internal_link(obj, 'test123'), 'test123')
|
self.assertEqual(report_tags.internal_link(obj, 'test123'), 'test123')
|
||||||
link = report_tags.internal_link(obj.get_absolute_url(), 'test')
|
link = report_tags.internal_link(obj.get_absolute_url(), 'test')
|
||||||
self.assertEqual(
|
|
||||||
link, f'<a href="http://localhost:8000/platform/part/{obj.pk}">test</a>'
|
# Test might return one of two results, depending on test env
|
||||||
)
|
# If INVENTREE_SITE_URL is not set in the CI environment, the link will be relative
|
||||||
|
options = [
|
||||||
|
f'<a href="http://localhost:8000/platform/part/{obj.pk}">test</a>',
|
||||||
|
f'<a href="/platform/part/{obj.pk}">test</a>',
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertIn(link, options)
|
||||||
|
|
||||||
# Test with an invalid object
|
# Test with an invalid object
|
||||||
link = report_tags.internal_link(None, None)
|
link = report_tags.internal_link(None, None)
|
||||||
|
@ -277,6 +277,74 @@ class ReportTest(InvenTreeAPITestCase):
|
|||||||
template.refresh_from_db()
|
template.refresh_from_db()
|
||||||
self.assertEqual(template.description, 'An updated description')
|
self.assertEqual(template.description, 'An updated description')
|
||||||
|
|
||||||
|
def test_print(self):
|
||||||
|
"""Test that we can print a report manually."""
|
||||||
|
# Find a suitable report template
|
||||||
|
template = ReportTemplate.objects.filter(
|
||||||
|
enabled=True, model_type='stockitem'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# Gather some items
|
||||||
|
items = StockItem.objects.all()[0:5]
|
||||||
|
|
||||||
|
output = template.print(items)
|
||||||
|
|
||||||
|
self.assertTrue(output.complete)
|
||||||
|
self.assertEqual(output.items, 5)
|
||||||
|
self.assertIsNotNone(output.output)
|
||||||
|
self.assertTrue(output.output.name.endswith('.pdf'))
|
||||||
|
|
||||||
|
|
||||||
|
class LabelTest(InvenTreeAPITestCase):
|
||||||
|
"""Unit tests for label templates."""
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'part',
|
||||||
|
'company',
|
||||||
|
'location',
|
||||||
|
'test_templates',
|
||||||
|
'supplier_part',
|
||||||
|
'stock',
|
||||||
|
'stock_tests',
|
||||||
|
'bom',
|
||||||
|
'build',
|
||||||
|
'order',
|
||||||
|
'return_order',
|
||||||
|
'sales_order',
|
||||||
|
]
|
||||||
|
|
||||||
|
superuser = True
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Ensure cache is cleared as part of test setup."""
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
apps.get_app_config('report').create_default_labels()
|
||||||
|
|
||||||
|
return super().setUp()
|
||||||
|
|
||||||
|
def test_print(self):
|
||||||
|
"""Test manual printing of label templates."""
|
||||||
|
# Find a suitable label template
|
||||||
|
template = LabelTemplate.objects.filter(enabled=True, model_type='part').first()
|
||||||
|
|
||||||
|
# Gather some items
|
||||||
|
parts = Part.objects.all()[0:10]
|
||||||
|
|
||||||
|
# Find the label plugin (render to pdf)
|
||||||
|
plugin = registry.get_plugin('inventreelabel')
|
||||||
|
|
||||||
|
self.assertIsNotNone(template)
|
||||||
|
self.assertIsNotNone(plugin)
|
||||||
|
|
||||||
|
output = template.print(items=parts, plugin=plugin)
|
||||||
|
|
||||||
|
self.assertTrue(output.complete)
|
||||||
|
self.assertEqual(output.items, 10)
|
||||||
|
self.assertIsNotNone(output.output)
|
||||||
|
self.assertTrue(output.output.name.endswith('.pdf'))
|
||||||
|
|
||||||
|
|
||||||
class PrintTestMixins:
|
class PrintTestMixins:
|
||||||
"""Mixin that enables e2e printing tests."""
|
"""Mixin that enables e2e printing tests."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user