mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-13 18:43:08 +00:00
[WIP] Background reports (#9199)
* Update report generation progress * Add shim task for offloading report printing * Cleanup * Add detail endpoints for label and report outputs * Display report printing progress in UI * Implement similar for label printing * Reduce output for CI * Add plugin slug * Bump API version * Ensure it works with machine printing * Fix null comparison * Fix SKU link * Update playwright tests * Massively reduce log output when printing
This commit is contained in:
parent
d5a176c121
commit
d822b9b574
10
.github/workflows/qc_checks.yaml
vendored
10
.github/workflows/qc_checks.yaml
vendored
@ -273,7 +273,7 @@ jobs:
|
|||||||
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
||||||
INVENTREE_SITE_URL: http://127.0.0.1:12345
|
INVENTREE_SITE_URL: http://127.0.0.1:12345
|
||||||
INVENTREE_DEBUG: true
|
INVENTREE_DEBUG: true
|
||||||
INVENTREE_LOG_LEVEL: INFO
|
INVENTREE_LOG_LEVEL: WARNING
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
|
||||||
@ -358,7 +358,7 @@ jobs:
|
|||||||
INVENTREE_DB_HOST: "127.0.0.1"
|
INVENTREE_DB_HOST: "127.0.0.1"
|
||||||
INVENTREE_DB_PORT: 5432
|
INVENTREE_DB_PORT: 5432
|
||||||
INVENTREE_DEBUG: true
|
INVENTREE_DEBUG: true
|
||||||
INVENTREE_LOG_LEVEL: INFO
|
INVENTREE_LOG_LEVEL: WARNING
|
||||||
INVENTREE_CONSOLE_LOG: false
|
INVENTREE_CONSOLE_LOG: false
|
||||||
INVENTREE_CACHE_HOST: localhost
|
INVENTREE_CACHE_HOST: localhost
|
||||||
INVENTREE_PLUGINS_ENABLED: true
|
INVENTREE_PLUGINS_ENABLED: true
|
||||||
@ -408,7 +408,7 @@ jobs:
|
|||||||
INVENTREE_DB_HOST: "127.0.0.1"
|
INVENTREE_DB_HOST: "127.0.0.1"
|
||||||
INVENTREE_DB_PORT: 3306
|
INVENTREE_DB_PORT: 3306
|
||||||
INVENTREE_DEBUG: true
|
INVENTREE_DEBUG: true
|
||||||
INVENTREE_LOG_LEVEL: INFO
|
INVENTREE_LOG_LEVEL: WARNING
|
||||||
INVENTREE_CONSOLE_LOG: false
|
INVENTREE_CONSOLE_LOG: false
|
||||||
INVENTREE_PLUGINS_ENABLED: true
|
INVENTREE_PLUGINS_ENABLED: true
|
||||||
|
|
||||||
@ -455,7 +455,7 @@ jobs:
|
|||||||
INVENTREE_DB_HOST: "127.0.0.1"
|
INVENTREE_DB_HOST: "127.0.0.1"
|
||||||
INVENTREE_DB_PORT: 5432
|
INVENTREE_DB_PORT: 5432
|
||||||
INVENTREE_DEBUG: true
|
INVENTREE_DEBUG: true
|
||||||
INVENTREE_LOG_LEVEL: INFO
|
INVENTREE_LOG_LEVEL: WARNING
|
||||||
INVENTREE_PLUGINS_ENABLED: false
|
INVENTREE_PLUGINS_ENABLED: false
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@ -498,7 +498,7 @@ jobs:
|
|||||||
INVENTREE_DB_ENGINE: sqlite3
|
INVENTREE_DB_ENGINE: sqlite3
|
||||||
INVENTREE_DB_NAME: /home/runner/work/InvenTree/db.sqlite3
|
INVENTREE_DB_NAME: /home/runner/work/InvenTree/db.sqlite3
|
||||||
INVENTREE_DEBUG: true
|
INVENTREE_DEBUG: true
|
||||||
INVENTREE_LOG_LEVEL: INFO
|
INVENTREE_LOG_LEVEL: WARNING
|
||||||
INVENTREE_PLUGINS_ENABLED: false
|
INVENTREE_PLUGINS_ENABLED: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 318
|
INVENTREE_API_VERSION = 319
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v319 - 2025-03-04 : https://github.com/inventree/InvenTree/pull/9199
|
||||||
|
- Add detail API endpoint for the LabelOutput model
|
||||||
|
- Add detail API endpoint for the ReportOutput model
|
||||||
|
|
||||||
v318 - 2025-02-25 : https://github.com/inventree/InvenTree/pull/9116
|
v318 - 2025-02-25 : https://github.com/inventree/InvenTree/pull/9116
|
||||||
- Adds user profile API endpoints
|
- Adds user profile API endpoints
|
||||||
|
|
||||||
|
@ -193,12 +193,12 @@ class LabelPrintingMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update the progress of the print job
|
# Update the progress of the print job
|
||||||
output.progress += int(100 / N)
|
output.progress += 1
|
||||||
output.save()
|
output.save()
|
||||||
|
|
||||||
# Mark the output as complete
|
# Mark the output as complete
|
||||||
output.complete = True
|
output.complete = True
|
||||||
output.progress = 100
|
output.progress = N
|
||||||
|
|
||||||
# Add in the generated file (if applicable)
|
# Add in the generated file (if applicable)
|
||||||
output.output = self.get_generated_file(**print_args)
|
output.output = self.get_generated_file(**print_args)
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@ -79,18 +78,26 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin):
|
|||||||
'printing_options': kwargs['printing_options'].get('driver_options', {}),
|
'printing_options': kwargs['printing_options'].get('driver_options', {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
# save the current used printer as last used printer
|
if output:
|
||||||
# only the last ten used printers are saved so that this list doesn't grow infinitely
|
user = output.user
|
||||||
last_used_printers = get_last_used_printers(request.user)
|
elif request:
|
||||||
machine_pk = str(machine.pk)
|
user = request.user
|
||||||
if machine_pk in last_used_printers:
|
else:
|
||||||
last_used_printers.remove(machine_pk)
|
user = None
|
||||||
last_used_printers.insert(0, machine_pk)
|
|
||||||
InvenTreeUserSetting.set_setting(
|
# Save the current used printer as last used printer
|
||||||
'LAST_USED_PRINTING_MACHINES',
|
# Only the last ten used printers are saved so that this list doesn't grow infinitely
|
||||||
','.join(last_used_printers[:10]),
|
if user and user.is_authenticated:
|
||||||
user=request.user,
|
last_used_printers = get_last_used_printers(user)
|
||||||
)
|
machine_pk = str(machine.pk)
|
||||||
|
if machine_pk in last_used_printers:
|
||||||
|
last_used_printers.remove(machine_pk)
|
||||||
|
last_used_printers.insert(0, machine_pk)
|
||||||
|
InvenTreeUserSetting.set_setting(
|
||||||
|
'LAST_USED_PRINTING_MACHINES',
|
||||||
|
','.join(last_used_printers[:10]),
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
call_machine_function,
|
call_machine_function,
|
||||||
@ -98,15 +105,17 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin):
|
|||||||
'print_labels',
|
'print_labels',
|
||||||
label,
|
label,
|
||||||
items,
|
items,
|
||||||
|
output=output,
|
||||||
force_sync=settings.TESTING or driver.USE_BACKGROUND_WORKER,
|
force_sync=settings.TESTING or driver.USE_BACKGROUND_WORKER,
|
||||||
group='plugin',
|
group='plugin',
|
||||||
**print_kwargs,
|
**print_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
return JsonResponse({
|
# Inform the user that the process has been offloaded to the printer
|
||||||
'success': True,
|
if output:
|
||||||
'message': f'{len(items)} labels printed',
|
output.output = None
|
||||||
})
|
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."""
|
||||||
|
@ -136,6 +136,10 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
|
|||||||
|
|
||||||
idx += n_cells
|
idx += n_cells
|
||||||
|
|
||||||
|
# Update printing progress
|
||||||
|
output.progress += 1
|
||||||
|
output.save()
|
||||||
|
|
||||||
if len(pages) == 0:
|
if len(pages) == 0:
|
||||||
raise ValidationError(_('No labels were generated'))
|
raise ValidationError(_('No labels were generated'))
|
||||||
|
|
||||||
@ -152,7 +156,7 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
|
|||||||
|
|
||||||
output.output = ContentFile(document, 'labels.pdf')
|
output.output = ContentFile(document, 'labels.pdf')
|
||||||
|
|
||||||
output.progress = 100
|
output.progress = n_labels
|
||||||
output.complete = True
|
output.complete = True
|
||||||
output.save()
|
output.save()
|
||||||
|
|
||||||
|
@ -194,16 +194,32 @@ class LabelPrint(GenericAPIView):
|
|||||||
|
|
||||||
def print(self, template, items_to_print, plugin, request):
|
def print(self, template, items_to_print, plugin, request):
|
||||||
"""Print this label template against a number of provided items."""
|
"""Print this label template against a number of provided items."""
|
||||||
|
import report.tasks
|
||||||
|
from InvenTree.tasks import offload_task
|
||||||
|
|
||||||
if plugin_serializer := plugin.get_printing_options_serializer(
|
if plugin_serializer := plugin.get_printing_options_serializer(
|
||||||
request, data=request.data, context=self.get_serializer_context()
|
request, data=request.data, context=self.get_serializer_context()
|
||||||
):
|
):
|
||||||
plugin_serializer.is_valid(raise_exception=True)
|
plugin_serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
output = template.print(
|
# Generate a new LabelOutput object to print against
|
||||||
items_to_print,
|
output = report.models.LabelOutput.objects.create(
|
||||||
plugin,
|
template=template,
|
||||||
|
plugin=plugin.slug,
|
||||||
|
user=request.user,
|
||||||
|
progress=0,
|
||||||
|
items=len(items_to_print),
|
||||||
|
complete=False,
|
||||||
|
output=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
offload_task(
|
||||||
|
report.tasks.print_labels,
|
||||||
|
template.pk,
|
||||||
|
[item.pk for item in items_to_print],
|
||||||
|
output.pk,
|
||||||
|
plugin.slug,
|
||||||
options=(plugin_serializer.data if plugin_serializer else {}),
|
options=(plugin_serializer.data if plugin_serializer else {}),
|
||||||
request=request,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
output.refresh_from_db()
|
output.refresh_from_db()
|
||||||
@ -255,8 +271,30 @@ class ReportPrint(GenericAPIView):
|
|||||||
return self.print(template, instances, request)
|
return self.print(template, instances, request)
|
||||||
|
|
||||||
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.
|
||||||
output = template.print(items_to_print, request)
|
|
||||||
|
This functionality is offloaded to the background worker process,
|
||||||
|
which will update the status of the ReportOutput object as it progresses.
|
||||||
|
"""
|
||||||
|
import report.tasks
|
||||||
|
from InvenTree.tasks import offload_task
|
||||||
|
|
||||||
|
# Generate a new ReportOutput object
|
||||||
|
output = report.models.ReportOutput.objects.create(
|
||||||
|
template=template,
|
||||||
|
user=request.user,
|
||||||
|
progress=0,
|
||||||
|
items=len(items_to_print),
|
||||||
|
complete=False,
|
||||||
|
output=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
output.refresh_from_db()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
report.serializers.ReportOutputSerializer(output).data, status=201
|
report.serializers.ReportOutputSerializer(output).data, status=201
|
||||||
@ -317,24 +355,36 @@ class TemplateOutputMixin:
|
|||||||
ordering_field_aliases = {'model_type': 'template__model_type'}
|
ordering_field_aliases = {'model_type': 'template__model_type'}
|
||||||
|
|
||||||
|
|
||||||
class LabelOutputList(
|
class LabelOutputMixin(TemplatePermissionMixin, TemplateOutputMixin):
|
||||||
TemplatePermissionMixin, TemplateOutputMixin, BulkDeleteMixin, ListAPI
|
"""Mixin class for a label output API endpoint."""
|
||||||
):
|
|
||||||
"""List endpoint for LabelOutput objects."""
|
|
||||||
|
|
||||||
queryset = report.models.LabelOutput.objects.all()
|
queryset = report.models.LabelOutput.objects.all()
|
||||||
serializer_class = report.serializers.LabelOutputSerializer
|
serializer_class = report.serializers.LabelOutputSerializer
|
||||||
|
|
||||||
|
|
||||||
class ReportOutputList(
|
class LabelOutputList(LabelOutputMixin, BulkDeleteMixin, ListAPI):
|
||||||
TemplatePermissionMixin, TemplateOutputMixin, BulkDeleteMixin, ListAPI
|
"""List endpoint for LabelOutput objects."""
|
||||||
):
|
|
||||||
"""List endpoint for ReportOutput objects."""
|
|
||||||
|
class LabelOutputDetail(LabelOutputMixin, RetrieveUpdateDestroyAPI):
|
||||||
|
"""Detail endpoint for LabelOutput objects."""
|
||||||
|
|
||||||
|
|
||||||
|
class ReportOutputMixin(TemplatePermissionMixin, TemplateOutputMixin):
|
||||||
|
"""Mixin class for a report output API endpoint."""
|
||||||
|
|
||||||
queryset = report.models.ReportOutput.objects.all()
|
queryset = report.models.ReportOutput.objects.all()
|
||||||
serializer_class = report.serializers.ReportOutputSerializer
|
serializer_class = report.serializers.ReportOutputSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ReportOutputList(ReportOutputMixin, BulkDeleteMixin, ListAPI):
|
||||||
|
"""List endpoint for ReportOutput objects."""
|
||||||
|
|
||||||
|
|
||||||
|
class ReportOutputDetail(ReportOutputMixin, RetrieveUpdateDestroyAPI):
|
||||||
|
"""Detail endpoint for ReportOutput objects."""
|
||||||
|
|
||||||
|
|
||||||
label_api_urls = [
|
label_api_urls = [
|
||||||
# Printing endpoint
|
# Printing endpoint
|
||||||
path('print/', LabelPrint.as_view(), name='api-label-print'),
|
path('print/', LabelPrint.as_view(), name='api-label-print'),
|
||||||
@ -364,7 +414,12 @@ label_api_urls = [
|
|||||||
# Label outputs
|
# Label outputs
|
||||||
path(
|
path(
|
||||||
'output/',
|
'output/',
|
||||||
include([path('', LabelOutputList.as_view(), name='api-label-output-list')]),
|
include([
|
||||||
|
path(
|
||||||
|
'<int:pk>/', LabelOutputDetail.as_view(), name='api-label-output-detail'
|
||||||
|
),
|
||||||
|
path('', LabelOutputList.as_view(), name='api-label-output-list'),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -397,7 +452,14 @@ report_api_urls = [
|
|||||||
# Generated report outputs
|
# Generated report outputs
|
||||||
path(
|
path(
|
||||||
'output/',
|
'output/',
|
||||||
include([path('', ReportOutputList.as_view(), name='api-report-output-list')]),
|
include([
|
||||||
|
path(
|
||||||
|
'<int:pk>/',
|
||||||
|
ReportOutputDetail.as_view(),
|
||||||
|
name='api-report-output-detail',
|
||||||
|
),
|
||||||
|
path('', ReportOutputList.as_view(), name='api-report-output-list'),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
# Report assets
|
# Report assets
|
||||||
path(
|
path(
|
||||||
|
@ -10,12 +10,13 @@ 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.db.utils import IntegrityError, OperationalError, ProgrammingError
|
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
|
||||||
|
|
||||||
|
import structlog
|
||||||
from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode
|
from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode
|
||||||
|
|
||||||
import InvenTree.exceptions
|
import InvenTree.exceptions
|
||||||
import InvenTree.ready
|
import InvenTree.ready
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = structlog.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
class ReportConfig(AppConfig):
|
class ReportConfig(AppConfig):
|
||||||
@ -26,8 +27,12 @@ class ReportConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
"""This function is called whenever the app is loaded."""
|
"""This function is called whenever the app is loaded."""
|
||||||
# Configure logging for PDF generation (disable "info" messages)
|
# Configure logging for PDF generation (disable "info" messages)
|
||||||
logging.getLogger('fontTools').setLevel(logging.WARNING)
|
|
||||||
logging.getLogger('weasyprint').setLevel(logging.WARNING)
|
# Reduce log output for fontTools and weasyprint
|
||||||
|
for name, log_manager in logging.root.manager.loggerDict.items():
|
||||||
|
if name.lower().startswith(('fonttools', 'weasyprint')):
|
||||||
|
if hasattr(log_manager, 'setLevel'):
|
||||||
|
log_manager.setLevel(logging.WARNING)
|
||||||
|
|
||||||
super().ready()
|
super().ready()
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"""Report template model definitions."""
|
"""Report template model definitions."""
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -19,6 +18,8 @@ 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 _
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
|
||||||
import InvenTree.exceptions
|
import InvenTree.exceptions
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.models
|
import InvenTree.models
|
||||||
@ -39,7 +40,7 @@ except OSError as err: # pragma: no cover
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = structlog.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def dummy_print_request() -> HttpRequest:
|
def dummy_print_request() -> HttpRequest:
|
||||||
@ -367,11 +368,12 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def print(self, items: list, request=None, **kwargs) -> 'ReportOutput':
|
def print(self, items: list, request=None, output=None, **kwargs) -> 'ReportOutput':
|
||||||
"""Print reports for a list of items against this template.
|
"""Print reports for a list of items against this template.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
items: A list of items to print reports for (model instance)
|
items: A list of items to print reports for (model instance)
|
||||||
|
output: The ReportOutput object to use (if provided)
|
||||||
request: The request object (optional)
|
request: The request object (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -390,6 +392,8 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
- Render using background worker, provide progress updates to UI
|
- Render using background worker, provide progress updates to UI
|
||||||
- Run report generation in the background worker process
|
- Run report generation in the background worker process
|
||||||
"""
|
"""
|
||||||
|
logger.info("Printing %s reports against template '%s'", len(items), self.name)
|
||||||
|
|
||||||
outputs = []
|
outputs = []
|
||||||
|
|
||||||
debug_mode = get_global_setting('REPORT_DEBUG_MODE', False)
|
debug_mode = get_global_setting('REPORT_DEBUG_MODE', False)
|
||||||
@ -403,6 +407,23 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
|
|
||||||
report_plugins = registry.with_mixin('report')
|
report_plugins = registry.with_mixin('report')
|
||||||
|
|
||||||
|
# If a ReportOutput object is not provided, create a new one
|
||||||
|
if not output:
|
||||||
|
output = ReportOutput.objects.create(
|
||||||
|
template=self,
|
||||||
|
items=len(items),
|
||||||
|
user=request.user
|
||||||
|
if request.user and request.user.is_authenticated
|
||||||
|
else None,
|
||||||
|
progress=0,
|
||||||
|
complete=False,
|
||||||
|
output=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if output.progress != 0:
|
||||||
|
output.progress = 0
|
||||||
|
output.save()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for instance in items:
|
for instance in items:
|
||||||
context = self.get_context(instance, request)
|
context = self.get_context(instance, request)
|
||||||
@ -413,18 +434,18 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
# Render the report output
|
# Render the report output
|
||||||
try:
|
try:
|
||||||
if debug_mode:
|
if debug_mode:
|
||||||
output = self.render_as_string(instance, request)
|
report = self.render_as_string(instance, request)
|
||||||
else:
|
else:
|
||||||
output = self.render(instance, request)
|
report = self.render(instance, request)
|
||||||
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')
|
raise ValidationError(f'Template file {t_name} does not exist')
|
||||||
|
|
||||||
outputs.append(output)
|
outputs.append(report)
|
||||||
|
|
||||||
# Attach the generated report to the model instance (if required)
|
# Attach the generated report to the model instance (if required)
|
||||||
if self.attach_to_model and not debug_mode:
|
if self.attach_to_model and not debug_mode:
|
||||||
data = output.get_document().write_pdf()
|
data = report.get_document().write_pdf()
|
||||||
instance.create_attachment(
|
instance.create_attachment(
|
||||||
attachment=ContentFile(data, report_name),
|
attachment=ContentFile(data, report_name),
|
||||||
comment=_(f'Report generated from template {self.name}'),
|
comment=_(f'Report generated from template {self.name}'),
|
||||||
@ -436,11 +457,16 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
# Provide generated report to any interested plugins
|
# Provide generated report to any interested plugins
|
||||||
for plugin in report_plugins:
|
for plugin in report_plugins:
|
||||||
try:
|
try:
|
||||||
plugin.report_callback(self, instance, output, request)
|
plugin.report_callback(self, instance, report, request)
|
||||||
except Exception:
|
except Exception:
|
||||||
InvenTree.exceptions.log_error(
|
InvenTree.exceptions.log_error(
|
||||||
f'plugins.{plugin.slug}.report_callback'
|
f'plugins.{plugin.slug}.report_callback'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update the progress of the report generation
|
||||||
|
output.progress += 1
|
||||||
|
output.save()
|
||||||
|
|
||||||
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):
|
if get_global_setting('REPORT_LOG_ERRORS', backup_value=True):
|
||||||
@ -463,8 +489,8 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
pages = []
|
pages = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for output in outputs:
|
for report in outputs:
|
||||||
doc = output.get_document()
|
doc = report.get_document()
|
||||||
for page in doc.pages:
|
for page in doc.pages:
|
||||||
pages.append(page)
|
pages.append(page)
|
||||||
|
|
||||||
@ -474,14 +500,9 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
raise ValidationError(f'Template file {t_name} does not exist')
|
raise ValidationError(f'Template file {t_name} does not exist')
|
||||||
|
|
||||||
# Save the generated report to the database
|
# Save the generated report to the database
|
||||||
output = ReportOutput.objects.create(
|
output.complete = True
|
||||||
template=self,
|
output.output = ContentFile(data, report_name)
|
||||||
items=len(items),
|
output.save()
|
||||||
user=request.user if request.user.is_authenticated else None,
|
|
||||||
progress=100,
|
|
||||||
complete=True,
|
|
||||||
output=ContentFile(data, report_name),
|
|
||||||
)
|
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@ -560,13 +581,20 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def print(
|
def print(
|
||||||
self, items: list, plugin: InvenTreePlugin, options=None, request=None, **kwargs
|
self,
|
||||||
|
items: list,
|
||||||
|
plugin: InvenTreePlugin,
|
||||||
|
output=None,
|
||||||
|
options=None,
|
||||||
|
request=None,
|
||||||
|
**kwargs,
|
||||||
) -> 'LabelOutput':
|
) -> 'LabelOutput':
|
||||||
"""Print labels for a list of items against this template.
|
"""Print labels for a list of items against this template.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
items: A list of items to print labels for (model instance)
|
items: A list of items to print labels for (model instance)
|
||||||
plugin: The plugin to use for label rendering
|
plugin: The plugin to use for label rendering
|
||||||
|
output: The LabelOutput object to use (if provided)
|
||||||
options: Additional options for the label printing plugin (optional)
|
options: Additional options for the label printing plugin (optional)
|
||||||
request: The request object (optional)
|
request: The request object (optional)
|
||||||
|
|
||||||
@ -576,15 +604,23 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
Raises:
|
Raises:
|
||||||
ValidationError: If there is an error during label printing
|
ValidationError: If there is an error during label printing
|
||||||
"""
|
"""
|
||||||
output = LabelOutput.objects.create(
|
logger.info(
|
||||||
template=self,
|
"Printing %s labels against template '%s' using plugin '%s'",
|
||||||
items=len(items),
|
len(items),
|
||||||
plugin=plugin.slug,
|
plugin.slug,
|
||||||
user=request.user if request else None,
|
self.name,
|
||||||
progress=0,
|
|
||||||
complete=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not output:
|
||||||
|
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:
|
if options is None:
|
||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
@ -609,8 +645,7 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
InvenTree.exceptions.log_error(f'plugins.{plugin.slug}.print_labels')
|
InvenTree.exceptions.log_error(f'plugins.{plugin.slug}.print_labels')
|
||||||
raise ValidationError([_('Error printing labels'), str(e)])
|
raise ValidationError([_('Error printing labels'), str(e)])
|
||||||
|
|
||||||
output.complete = True
|
output.refresh_from_db()
|
||||||
output.save()
|
|
||||||
|
|
||||||
# Return the output object
|
# Return the output object
|
||||||
return output
|
return output
|
||||||
|
@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
from InvenTree.exceptions import log_error
|
||||||
from InvenTree.helpers import current_time
|
from InvenTree.helpers import current_time
|
||||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||||
from report.models import LabelOutput, ReportOutput
|
from report.models import LabelOutput, ReportOutput
|
||||||
|
|
||||||
|
logger = structlog.get_logger('inventree')
|
||||||
|
|
||||||
|
|
||||||
@scheduled_task(ScheduledTask.DAILY)
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
def cleanup_old_report_outputs():
|
def cleanup_old_report_outputs():
|
||||||
@ -15,3 +20,70 @@ def cleanup_old_report_outputs():
|
|||||||
|
|
||||||
LabelOutput.objects.filter(created__lte=threshold).delete()
|
LabelOutput.objects.filter(created__lte=threshold).delete()
|
||||||
ReportOutput.objects.filter(created__lte=threshold).delete()
|
ReportOutput.objects.filter(created__lte=threshold).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def print_reports(template_id: int, item_ids: list[int], output_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 ReportOutput to use (if provided)
|
||||||
|
|
||||||
|
This function is intended to be called by the background worker,
|
||||||
|
and will continuously update the status of the ReportOutput object.
|
||||||
|
"""
|
||||||
|
from report.models import ReportOutput, ReportTemplate
|
||||||
|
|
||||||
|
try:
|
||||||
|
template = ReportTemplate.objects.get(pk=template_id)
|
||||||
|
output = ReportOutput.objects.get(pk=output_id)
|
||||||
|
except Exception:
|
||||||
|
log_error('report.tasks.print_reports')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fetch the items to be included in the report
|
||||||
|
model = template.get_model()
|
||||||
|
items = model.objects.filter(pk__in=item_ids)
|
||||||
|
|
||||||
|
template.print(items, output=output)
|
||||||
|
|
||||||
|
|
||||||
|
def print_labels(
|
||||||
|
template_id: int, item_ids: list[int], output_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 LabelOutput to use (if provided)
|
||||||
|
plugin_slug: The ID of the LabelPlugin to use (if provided)
|
||||||
|
|
||||||
|
This function is intended to be called by the background worker,
|
||||||
|
and will continuously update the status of the LabelOutput object.
|
||||||
|
"""
|
||||||
|
from plugin.registry import registry
|
||||||
|
from report.models import LabelOutput, LabelTemplate
|
||||||
|
|
||||||
|
try:
|
||||||
|
template = LabelTemplate.objects.get(pk=template_id)
|
||||||
|
output = LabelOutput.objects.get(pk=output_id)
|
||||||
|
except Exception:
|
||||||
|
log_error('report.tasks.print_labels')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fetch the items to be included in the report
|
||||||
|
model = template.get_model()
|
||||||
|
items = model.objects.filter(pk__in=item_ids)
|
||||||
|
|
||||||
|
plugin = registry.get_plugin(plugin_slug)
|
||||||
|
|
||||||
|
if not plugin:
|
||||||
|
logger.warning("Label printing plugin '%s' not found", plugin_slug)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Extract optional arguments for label printing
|
||||||
|
options = kwargs.pop('options') or {}
|
||||||
|
|
||||||
|
template.print(items, plugin, output=output, options=options)
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications, showNotification } from '@mantine/notifications';
|
||||||
import { IconPrinter, IconReport, IconTags } from '@tabler/icons-react';
|
import {
|
||||||
|
IconCircleCheck,
|
||||||
|
IconPrinter,
|
||||||
|
IconReport,
|
||||||
|
IconTags
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
|
import { useApi } from '../../contexts/ApiContext';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import type { ModelType } from '../../enums/ModelType';
|
import type { ModelType } from '../../enums/ModelType';
|
||||||
import { extractAvailableFields } from '../../functions/forms';
|
import { extractAvailableFields } from '../../functions/forms';
|
||||||
@ -17,6 +22,94 @@ import {
|
|||||||
} from '../../states/SettingsState';
|
} from '../../states/SettingsState';
|
||||||
import type { ApiFormFieldSet } from '../forms/fields/ApiFormField';
|
import type { ApiFormFieldSet } from '../forms/fields/ApiFormField';
|
||||||
import { ActionDropdown } from '../items/ActionDropdown';
|
import { ActionDropdown } from '../items/ActionDropdown';
|
||||||
|
import { ProgressBar } from '../items/ProgressBar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to track the progress of a printing operation
|
||||||
|
*/
|
||||||
|
function usePrintingProgress({
|
||||||
|
title,
|
||||||
|
outputId,
|
||||||
|
endpoint
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
outputId?: number;
|
||||||
|
endpoint: ApiEndpoints;
|
||||||
|
}) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!outputId) {
|
||||||
|
setLoading(true);
|
||||||
|
showNotification({
|
||||||
|
id: `printing-progress-${endpoint}-${outputId}`,
|
||||||
|
title: title,
|
||||||
|
loading: true,
|
||||||
|
autoClose: false,
|
||||||
|
withCloseButton: false,
|
||||||
|
message: <ProgressBar size='lg' value={0} progressLabel />
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [outputId, endpoint, title]);
|
||||||
|
|
||||||
|
const progress = useQuery({
|
||||||
|
enabled: !!outputId && loading,
|
||||||
|
refetchInterval: 750,
|
||||||
|
queryKey: ['printingProgress', endpoint, outputId],
|
||||||
|
queryFn: () =>
|
||||||
|
api
|
||||||
|
.get(apiUrl(endpoint, outputId))
|
||||||
|
.then((response) => {
|
||||||
|
const data = response?.data ?? {};
|
||||||
|
|
||||||
|
if (data.pk && data.pk == outputId) {
|
||||||
|
if (data.complete) {
|
||||||
|
setLoading(false);
|
||||||
|
notifications.hide(`printing-progress-${endpoint}-${outputId}`);
|
||||||
|
notifications.hide('print-success');
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
id: 'print-success',
|
||||||
|
title: t`Printing`,
|
||||||
|
message: t`Printing completed successfully`,
|
||||||
|
color: 'green',
|
||||||
|
icon: <IconCircleCheck />
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.output) {
|
||||||
|
const url = generateUrl(data.output);
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notifications.update({
|
||||||
|
id: `printing-progress-${endpoint}-${outputId}`,
|
||||||
|
autoClose: false,
|
||||||
|
withCloseButton: false,
|
||||||
|
message: (
|
||||||
|
<ProgressBar
|
||||||
|
size='lg'
|
||||||
|
value={data.progress}
|
||||||
|
maximum={data.items}
|
||||||
|
progressLabel
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.hide(`printing-progress-${endpoint}-${outputId}`);
|
||||||
|
setLoading(false);
|
||||||
|
return {};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function PrintingActions({
|
export function PrintingActions({
|
||||||
items,
|
items,
|
||||||
@ -46,6 +139,21 @@ export function PrintingActions({
|
|||||||
return enableReports && globalSettings.isSet('REPORT_ENABLE');
|
return enableReports && globalSettings.isSet('REPORT_ENABLE');
|
||||||
}, [enableReports, globalSettings]);
|
}, [enableReports, globalSettings]);
|
||||||
|
|
||||||
|
const [labelId, setLabelId] = useState<number | undefined>(undefined);
|
||||||
|
const [reportId, setReportId] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const labelProgress = usePrintingProgress({
|
||||||
|
title: t`Printing Labels`,
|
||||||
|
outputId: labelId,
|
||||||
|
endpoint: ApiEndpoints.label_output
|
||||||
|
});
|
||||||
|
|
||||||
|
const reportProgress = usePrintingProgress({
|
||||||
|
title: t`Printing Reports`,
|
||||||
|
outputId: reportId,
|
||||||
|
endpoint: ApiEndpoints.report_output
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch available printing fields via OPTIONS request
|
// Fetch available printing fields via OPTIONS request
|
||||||
const printingFields = useQuery({
|
const printingFields = useQuery({
|
||||||
enabled: labelPrintingEnabled,
|
enabled: labelPrintingEnabled,
|
||||||
@ -106,36 +214,22 @@ export function PrintingActions({
|
|||||||
url: apiUrl(ApiEndpoints.label_print),
|
url: apiUrl(ApiEndpoints.label_print),
|
||||||
title: t`Print Label`,
|
title: t`Print Label`,
|
||||||
fields: labelFields,
|
fields: labelFields,
|
||||||
timeout: (items.length + 1) * 5000,
|
timeout: 5000,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
setPluginKey('');
|
setPluginKey('');
|
||||||
},
|
},
|
||||||
submitText: t`Print`,
|
submitText: t`Print`,
|
||||||
successMessage: t`Label printing completed successfully`,
|
successMessage: null,
|
||||||
onFormSuccess: (response: any) => {
|
onFormSuccess: (response: any) => {
|
||||||
setPluginKey('');
|
setPluginKey('');
|
||||||
if (!response.complete) {
|
setLabelId(response.pk);
|
||||||
// TODO: Periodically check for completion (requires server-side changes)
|
|
||||||
notifications.show({
|
|
||||||
title: t`Error`,
|
|
||||||
message: t`The label could not be generated`,
|
|
||||||
color: 'red'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.output) {
|
|
||||||
// An output file was generated
|
|
||||||
const url = generateUrl(response.output);
|
|
||||||
window.open(url.toString(), '_blank');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const reportModal = useCreateApiFormModal({
|
const reportModal = useCreateApiFormModal({
|
||||||
title: t`Print Report`,
|
title: t`Print Report`,
|
||||||
url: apiUrl(ApiEndpoints.report_print),
|
url: apiUrl(ApiEndpoints.report_print),
|
||||||
timeout: (items.length + 1) * 5000,
|
timeout: 5000,
|
||||||
fields: {
|
fields: {
|
||||||
template: {
|
template: {
|
||||||
filters: {
|
filters: {
|
||||||
@ -149,24 +243,10 @@ export function PrintingActions({
|
|||||||
value: items
|
value: items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitText: t`Generate`,
|
submitText: t`Print`,
|
||||||
successMessage: t`Report printing completed successfully`,
|
successMessage: null,
|
||||||
onFormSuccess: (response: any) => {
|
onFormSuccess: (response: any) => {
|
||||||
if (!response.complete) {
|
setReportId(response.pk);
|
||||||
// TODO: Periodically check for completion (requires server-side changes)
|
|
||||||
notifications.show({
|
|
||||||
title: t`Error`,
|
|
||||||
message: t`The report could not be generated`,
|
|
||||||
color: 'red'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.output) {
|
|
||||||
// An output file was generated
|
|
||||||
const url = generateUrl(response.output);
|
|
||||||
window.open(url.toString(), '_blank');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ export interface ApiFormProps {
|
|||||||
preFormWarning?: string;
|
preFormWarning?: string;
|
||||||
preFormSuccess?: string;
|
preFormSuccess?: string;
|
||||||
postFormContent?: JSX.Element;
|
postFormContent?: JSX.Element;
|
||||||
successMessage?: string;
|
successMessage?: string | null;
|
||||||
onFormSuccess?: (data: any) => void;
|
onFormSuccess?: (data: any) => void;
|
||||||
onFormError?: (response: any) => void;
|
onFormError?: (response: any) => void;
|
||||||
processFormData?: (data: any) => any;
|
processFormData?: (data: any) => any;
|
||||||
|
@ -84,7 +84,10 @@ export function useCreateApiFormModal(props: ApiFormModalProps) {
|
|||||||
() => ({
|
() => ({
|
||||||
...props,
|
...props,
|
||||||
fetchInitialData: props.fetchInitialData ?? false,
|
fetchInitialData: props.fetchInitialData ?? false,
|
||||||
successMessage: props.successMessage ?? t`Item Created`,
|
successMessage:
|
||||||
|
props.successMessage === null
|
||||||
|
? null
|
||||||
|
: (props.successMessage ?? t`Item Created`),
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
}),
|
}),
|
||||||
[props]
|
[props]
|
||||||
@ -101,7 +104,10 @@ export function useEditApiFormModal(props: ApiFormModalProps) {
|
|||||||
() => ({
|
() => ({
|
||||||
...props,
|
...props,
|
||||||
fetchInitialData: props.fetchInitialData ?? true,
|
fetchInitialData: props.fetchInitialData ?? true,
|
||||||
successMessage: props.successMessage ?? t`Item Updated`,
|
successMessage:
|
||||||
|
props.successMessage === null
|
||||||
|
? null
|
||||||
|
: (props.successMessage ?? t`Item Updated`),
|
||||||
method: 'PATCH'
|
method: 'PATCH'
|
||||||
}),
|
}),
|
||||||
[props]
|
[props]
|
||||||
@ -120,7 +126,10 @@ export function useDeleteApiFormModal(props: ApiFormModalProps) {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
submitText: t`Delete`,
|
submitText: t`Delete`,
|
||||||
submitColor: 'red',
|
submitColor: 'red',
|
||||||
successMessage: props.successMessage ?? t`Item Deleted`,
|
successMessage:
|
||||||
|
props.successMessage === null
|
||||||
|
? null
|
||||||
|
: (props.successMessage ?? t`Item Deleted`),
|
||||||
preFormContent: props.preFormContent ?? (
|
preFormContent: props.preFormContent ?? (
|
||||||
<Alert
|
<Alert
|
||||||
color={'red'}
|
color={'red'}
|
||||||
|
@ -122,7 +122,7 @@ export default function SupplierPartDetail() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const tr: DetailsField[] = [
|
const bl: DetailsField[] = [
|
||||||
{
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
name: 'supplier',
|
name: 'supplier',
|
||||||
@ -165,7 +165,7 @@ export default function SupplierPartDetail() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const bl: DetailsField[] = [
|
const br: DetailsField[] = [
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'packaging',
|
name: 'packaging',
|
||||||
@ -183,7 +183,7 @@ export default function SupplierPartDetail() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const br: DetailsField[] = [
|
const tr: DetailsField[] = [
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'in_stock',
|
name: 'in_stock',
|
||||||
@ -232,9 +232,9 @@ export default function SupplierPartDetail() {
|
|||||||
<DetailsTable title={t`Part Details`} fields={tl} item={data} />
|
<DetailsTable title={t`Part Details`} fields={tl} item={data} />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
<DetailsTable title={t`Supplier`} fields={tr} item={data} />
|
<DetailsTable title={t`Supplier`} fields={bl} item={data} />
|
||||||
<DetailsTable title={t`Packaging`} fields={bl} item={data} />
|
<DetailsTable title={t`Packaging`} fields={br} item={data} />
|
||||||
<DetailsTable title={t`Availability`} fields={br} item={data} />
|
<DetailsTable title={t`Availability`} fields={tr} item={data} />
|
||||||
</ItemDetailsGrid>
|
</ItemDetailsGrid>
|
||||||
);
|
);
|
||||||
}, [supplierPart, instanceQuery.isFetching]);
|
}, [supplierPart, instanceQuery.isFetching]);
|
||||||
|
@ -212,6 +212,7 @@ export default function StockDetail() {
|
|||||||
name: 'supplier_part',
|
name: 'supplier_part',
|
||||||
label: t`Supplier Part`,
|
label: t`Supplier Part`,
|
||||||
type: 'link',
|
type: 'link',
|
||||||
|
model_field: 'SKU',
|
||||||
model: ModelType.supplierpart,
|
model: ModelType.supplierpart,
|
||||||
hidden: !stockitem.supplier_part
|
hidden: !stockitem.supplier_part
|
||||||
},
|
},
|
||||||
|
@ -41,9 +41,7 @@ test('Label Printing', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Print', exact: true }).isEnabled();
|
await page.getByRole('button', { name: 'Print', exact: true }).isEnabled();
|
||||||
await page.getByRole('button', { name: 'Print', exact: true }).click();
|
await page.getByRole('button', { name: 'Print', exact: true }).click();
|
||||||
|
|
||||||
await page.locator('#form-success').waitFor();
|
await page.getByText('Printing completed successfully').first().waitFor();
|
||||||
await page.getByText('Label printing completed').waitFor();
|
|
||||||
|
|
||||||
await page.context().close();
|
await page.context().close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -75,12 +73,10 @@ test('Report Printing', async ({ page }) => {
|
|||||||
await page.waitForTimeout(100);
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
// Submit the print form (should result in success)
|
// Submit the print form (should result in success)
|
||||||
await page.getByRole('button', { name: 'Generate', exact: true }).isEnabled();
|
await page.getByRole('button', { name: 'Print', exact: true }).isEnabled();
|
||||||
await page.getByRole('button', { name: 'Generate', exact: true }).click();
|
await page.getByRole('button', { name: 'Print', exact: true }).click();
|
||||||
|
|
||||||
await page.locator('#form-success').waitFor();
|
|
||||||
await page.getByText('Report printing completed').waitFor();
|
|
||||||
|
|
||||||
|
await page.getByText('Printing completed successfully').first().waitFor();
|
||||||
await page.context().close();
|
await page.context().close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user