diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index fbf60e3e5f..2fe06a6a40 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -273,7 +273,7 @@ jobs: INVENTREE_PYTHON_TEST_PASSWORD: testpassword INVENTREE_SITE_URL: http://127.0.0.1:12345 INVENTREE_DEBUG: true - INVENTREE_LOG_LEVEL: INFO + INVENTREE_LOG_LEVEL: WARNING steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2 @@ -358,7 +358,7 @@ jobs: INVENTREE_DB_HOST: "127.0.0.1" INVENTREE_DB_PORT: 5432 INVENTREE_DEBUG: true - INVENTREE_LOG_LEVEL: INFO + INVENTREE_LOG_LEVEL: WARNING INVENTREE_CONSOLE_LOG: false INVENTREE_CACHE_HOST: localhost INVENTREE_PLUGINS_ENABLED: true @@ -408,7 +408,7 @@ jobs: INVENTREE_DB_HOST: "127.0.0.1" INVENTREE_DB_PORT: 3306 INVENTREE_DEBUG: true - INVENTREE_LOG_LEVEL: INFO + INVENTREE_LOG_LEVEL: WARNING INVENTREE_CONSOLE_LOG: false INVENTREE_PLUGINS_ENABLED: true @@ -455,7 +455,7 @@ jobs: INVENTREE_DB_HOST: "127.0.0.1" INVENTREE_DB_PORT: 5432 INVENTREE_DEBUG: true - INVENTREE_LOG_LEVEL: INFO + INVENTREE_LOG_LEVEL: WARNING INVENTREE_PLUGINS_ENABLED: false services: @@ -498,7 +498,7 @@ jobs: INVENTREE_DB_ENGINE: sqlite3 INVENTREE_DB_NAME: /home/runner/work/InvenTree/db.sqlite3 INVENTREE_DEBUG: true - INVENTREE_LOG_LEVEL: INFO + INVENTREE_LOG_LEVEL: WARNING INVENTREE_PLUGINS_ENABLED: false steps: diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index ad58146ce6..af73eb081a 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,17 @@ """InvenTree API version information.""" # 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.""" 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 - Adds user profile API endpoints diff --git a/src/backend/InvenTree/plugin/base/label/mixins.py b/src/backend/InvenTree/plugin/base/label/mixins.py index e6ef8ab5e7..2542b8ad64 100644 --- a/src/backend/InvenTree/plugin/base/label/mixins.py +++ b/src/backend/InvenTree/plugin/base/label/mixins.py @@ -193,12 +193,12 @@ class LabelPrintingMixin: ) # Update the progress of the print job - output.progress += int(100 / N) + output.progress += 1 output.save() # Mark the output as complete output.complete = True - output.progress = 100 + output.progress = N # Add in the generated file (if applicable) output.output = self.get_generated_file(**print_args) diff --git a/src/backend/InvenTree/plugin/builtin/labels/inventree_machine.py b/src/backend/InvenTree/plugin/builtin/labels/inventree_machine.py index 9c8e9dba24..f0b4add8ce 100644 --- a/src/backend/InvenTree/plugin/builtin/labels/inventree_machine.py +++ b/src/backend/InvenTree/plugin/builtin/labels/inventree_machine.py @@ -3,7 +3,6 @@ from typing import cast from django.conf import settings -from django.http import JsonResponse from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -79,18 +78,26 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin): 'printing_options': kwargs['printing_options'].get('driver_options', {}), } - # save the current used printer as last used printer - # only the last ten used printers are saved so that this list doesn't grow infinitely - last_used_printers = get_last_used_printers(request.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=request.user, - ) + if output: + user = output.user + elif request: + user = request.user + else: + user = None + + # Save the current used printer as last used printer + # Only the last ten used printers are saved so that this list doesn't grow infinitely + if user and user.is_authenticated: + 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( call_machine_function, @@ -98,15 +105,17 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin): 'print_labels', label, items, + output=output, force_sync=settings.TESTING or driver.USE_BACKGROUND_WORKER, group='plugin', **print_kwargs, ) - return JsonResponse({ - 'success': True, - 'message': f'{len(items)} labels printed', - }) + # Inform the user that the process has been offloaded to the printer + if output: + output.output = None + output.complete = True + output.save() class PrintingOptionsSerializer(serializers.Serializer): """Printing options serializer that adds a machine select and the machines options.""" diff --git a/src/backend/InvenTree/plugin/builtin/labels/label_sheet.py b/src/backend/InvenTree/plugin/builtin/labels/label_sheet.py index a4821805c7..ce904c40df 100644 --- a/src/backend/InvenTree/plugin/builtin/labels/label_sheet.py +++ b/src/backend/InvenTree/plugin/builtin/labels/label_sheet.py @@ -136,6 +136,10 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug idx += n_cells + # Update printing progress + output.progress += 1 + output.save() + if len(pages) == 0: raise ValidationError(_('No labels were generated')) @@ -152,7 +156,7 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug output.output = ContentFile(document, 'labels.pdf') - output.progress = 100 + output.progress = n_labels output.complete = True output.save() diff --git a/src/backend/InvenTree/report/api.py b/src/backend/InvenTree/report/api.py index d95601ffc5..b90d68a6a8 100644 --- a/src/backend/InvenTree/report/api.py +++ b/src/backend/InvenTree/report/api.py @@ -194,16 +194,32 @@ class LabelPrint(GenericAPIView): def print(self, template, items_to_print, plugin, request): """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( request, data=request.data, context=self.get_serializer_context() ): plugin_serializer.is_valid(raise_exception=True) - output = template.print( - items_to_print, - plugin, + # Generate a new LabelOutput object to print against + output = report.models.LabelOutput.objects.create( + 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 {}), - request=request, ) output.refresh_from_db() @@ -255,8 +271,30 @@ class ReportPrint(GenericAPIView): return self.print(template, instances, request) def print(self, template, items_to_print, request): - """Print this report template against a number of provided items.""" - output = template.print(items_to_print, request) + """Print this report template against a number of provided items. + + 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( report.serializers.ReportOutputSerializer(output).data, status=201 @@ -317,24 +355,36 @@ class TemplateOutputMixin: ordering_field_aliases = {'model_type': 'template__model_type'} -class LabelOutputList( - TemplatePermissionMixin, TemplateOutputMixin, BulkDeleteMixin, ListAPI -): - """List endpoint for LabelOutput objects.""" +class LabelOutputMixin(TemplatePermissionMixin, TemplateOutputMixin): + """Mixin class for a label output API endpoint.""" queryset = report.models.LabelOutput.objects.all() serializer_class = report.serializers.LabelOutputSerializer -class ReportOutputList( - TemplatePermissionMixin, TemplateOutputMixin, BulkDeleteMixin, ListAPI -): - """List endpoint for ReportOutput objects.""" +class LabelOutputList(LabelOutputMixin, BulkDeleteMixin, ListAPI): + """List endpoint for LabelOutput 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() 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 = [ # Printing endpoint path('print/', LabelPrint.as_view(), name='api-label-print'), @@ -364,7 +414,12 @@ label_api_urls = [ # Label outputs path( 'output/', - include([path('', LabelOutputList.as_view(), name='api-label-output-list')]), + include([ + path( + '/', 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 path( 'output/', - include([path('', ReportOutputList.as_view(), name='api-report-output-list')]), + include([ + path( + '/', + ReportOutputDetail.as_view(), + name='api-report-output-detail', + ), + path('', ReportOutputList.as_view(), name='api-report-output-list'), + ]), ), # Report assets path( diff --git a/src/backend/InvenTree/report/apps.py b/src/backend/InvenTree/report/apps.py index 0c11f9ca7a..43d8e09eb8 100644 --- a/src/backend/InvenTree/report/apps.py +++ b/src/backend/InvenTree/report/apps.py @@ -10,12 +10,13 @@ from django.core.files.base import ContentFile from django.core.files.storage import default_storage from django.db.utils import IntegrityError, OperationalError, ProgrammingError +import structlog from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode import InvenTree.exceptions import InvenTree.ready -logger = logging.getLogger('inventree') +logger = structlog.getLogger('inventree') class ReportConfig(AppConfig): @@ -26,8 +27,12 @@ class ReportConfig(AppConfig): def ready(self): """This function is called whenever the app is loaded.""" # 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() diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 02c7b266db..30e79c0ad0 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -1,6 +1,5 @@ """Report template model definitions.""" -import logging import os import sys @@ -19,6 +18,8 @@ from django.test.client import RequestFactory from django.urls import reverse from django.utils.translation import gettext_lazy as _ +import structlog + import InvenTree.exceptions import InvenTree.helpers import InvenTree.models @@ -39,7 +40,7 @@ except OSError as err: # pragma: no cover sys.exit(1) -logger = logging.getLogger('inventree') +logger = structlog.getLogger('inventree') def dummy_print_request() -> HttpRequest: @@ -367,11 +368,12 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): 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. Arguments: items: A list of items to print reports for (model instance) + output: The ReportOutput object to use (if provided) request: The request object (optional) Returns: @@ -390,6 +392,8 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): - Render using background worker, provide progress updates to UI - Run report generation in the background worker process """ + logger.info("Printing %s reports against template '%s'", len(items), self.name) + outputs = [] debug_mode = get_global_setting('REPORT_DEBUG_MODE', False) @@ -403,6 +407,23 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): 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: for instance in items: context = self.get_context(instance, request) @@ -413,18 +434,18 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): # Render the report output try: if debug_mode: - output = self.render_as_string(instance, request) + report = self.render_as_string(instance, request) else: - output = self.render(instance, request) + report = 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) + outputs.append(report) # 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() + data = report.get_document().write_pdf() instance.create_attachment( attachment=ContentFile(data, report_name), comment=_(f'Report generated from template {self.name}'), @@ -436,11 +457,16 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): # Provide generated report to any interested plugins for plugin in report_plugins: try: - plugin.report_callback(self, instance, output, request) + plugin.report_callback(self, instance, report, request) except Exception: InvenTree.exceptions.log_error( f'plugins.{plugin.slug}.report_callback' ) + + # Update the progress of the report generation + output.progress += 1 + output.save() + except Exception as exc: # Something went wrong during the report generation process if get_global_setting('REPORT_LOG_ERRORS', backup_value=True): @@ -463,8 +489,8 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): pages = [] try: - for output in outputs: - doc = output.get_document() + for report in outputs: + doc = report.get_document() for page in doc.pages: pages.append(page) @@ -474,14 +500,9 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): 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), - ) + output.complete = True + output.output = ContentFile(data, report_name) + output.save() return output @@ -560,13 +581,20 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase): return context 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': """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 + output: The LabelOutput object to use (if provided) options: Additional options for the label printing plugin (optional) request: The request object (optional) @@ -576,15 +604,23 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase): 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, + logger.info( + "Printing %s labels against template '%s' using plugin '%s'", + len(items), + plugin.slug, + self.name, ) + 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: options = {} @@ -609,8 +645,7 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase): InvenTree.exceptions.log_error(f'plugins.{plugin.slug}.print_labels') raise ValidationError([_('Error printing labels'), str(e)]) - output.complete = True - output.save() + output.refresh_from_db() # Return the output object return output diff --git a/src/backend/InvenTree/report/tasks.py b/src/backend/InvenTree/report/tasks.py index 606cd71304..ffddaa56fc 100644 --- a/src/backend/InvenTree/report/tasks.py +++ b/src/backend/InvenTree/report/tasks.py @@ -2,10 +2,15 @@ from datetime import timedelta +import structlog + +from InvenTree.exceptions import log_error from InvenTree.helpers import current_time from InvenTree.tasks import ScheduledTask, scheduled_task from report.models import LabelOutput, ReportOutput +logger = structlog.get_logger('inventree') + @scheduled_task(ScheduledTask.DAILY) def cleanup_old_report_outputs(): @@ -15,3 +20,70 @@ def cleanup_old_report_outputs(): LabelOutput.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) diff --git a/src/frontend/src/components/buttons/PrintingActions.tsx b/src/frontend/src/components/buttons/PrintingActions.tsx index 27c037d044..5bc136de63 100644 --- a/src/frontend/src/components/buttons/PrintingActions.tsx +++ b/src/frontend/src/components/buttons/PrintingActions.tsx @@ -1,10 +1,15 @@ import { t } from '@lingui/macro'; -import { notifications } from '@mantine/notifications'; -import { IconPrinter, IconReport, IconTags } from '@tabler/icons-react'; +import { notifications, showNotification } from '@mantine/notifications'; +import { + IconCircleCheck, + IconPrinter, + IconReport, + IconTags +} from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; -import { useMemo, useState } from 'react'; - +import { useEffect, useMemo, useState } from 'react'; import { api } from '../../App'; +import { useApi } from '../../contexts/ApiContext'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import type { ModelType } from '../../enums/ModelType'; import { extractAvailableFields } from '../../functions/forms'; @@ -17,6 +22,94 @@ import { } from '../../states/SettingsState'; import type { ApiFormFieldSet } from '../forms/fields/ApiFormField'; 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(false); + + useEffect(() => { + if (!!outputId) { + setLoading(true); + showNotification({ + id: `printing-progress-${endpoint}-${outputId}`, + title: title, + loading: true, + autoClose: false, + withCloseButton: false, + message: + }); + } 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: + }); + + 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: ( + + ) + }); + } + } + + return data; + }) + .catch(() => { + notifications.hide(`printing-progress-${endpoint}-${outputId}`); + setLoading(false); + return {}; + }) + }); +} export function PrintingActions({ items, @@ -46,6 +139,21 @@ export function PrintingActions({ return enableReports && globalSettings.isSet('REPORT_ENABLE'); }, [enableReports, globalSettings]); + const [labelId, setLabelId] = useState(undefined); + const [reportId, setReportId] = useState(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 const printingFields = useQuery({ enabled: labelPrintingEnabled, @@ -106,36 +214,22 @@ export function PrintingActions({ url: apiUrl(ApiEndpoints.label_print), title: t`Print Label`, fields: labelFields, - timeout: (items.length + 1) * 5000, + timeout: 5000, onClose: () => { setPluginKey(''); }, submitText: t`Print`, - successMessage: t`Label printing completed successfully`, + successMessage: null, onFormSuccess: (response: any) => { setPluginKey(''); - if (!response.complete) { - // 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'); - } + setLabelId(response.pk); } }); const reportModal = useCreateApiFormModal({ title: t`Print Report`, url: apiUrl(ApiEndpoints.report_print), - timeout: (items.length + 1) * 5000, + timeout: 5000, fields: { template: { filters: { @@ -149,24 +243,10 @@ export function PrintingActions({ value: items } }, - submitText: t`Generate`, - successMessage: t`Report printing completed successfully`, + submitText: t`Print`, + successMessage: null, onFormSuccess: (response: any) => { - if (!response.complete) { - // 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'); - } + setReportId(response.pk); } }); diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 1c2a5a7e5f..096f8739d1 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -92,7 +92,7 @@ export interface ApiFormProps { preFormWarning?: string; preFormSuccess?: string; postFormContent?: JSX.Element; - successMessage?: string; + successMessage?: string | null; onFormSuccess?: (data: any) => void; onFormError?: (response: any) => void; processFormData?: (data: any) => any; diff --git a/src/frontend/src/hooks/UseForm.tsx b/src/frontend/src/hooks/UseForm.tsx index bbbcdad4f7..abf97034dc 100644 --- a/src/frontend/src/hooks/UseForm.tsx +++ b/src/frontend/src/hooks/UseForm.tsx @@ -84,7 +84,10 @@ export function useCreateApiFormModal(props: ApiFormModalProps) { () => ({ ...props, fetchInitialData: props.fetchInitialData ?? false, - successMessage: props.successMessage ?? t`Item Created`, + successMessage: + props.successMessage === null + ? null + : (props.successMessage ?? t`Item Created`), method: 'POST' }), [props] @@ -101,7 +104,10 @@ export function useEditApiFormModal(props: ApiFormModalProps) { () => ({ ...props, fetchInitialData: props.fetchInitialData ?? true, - successMessage: props.successMessage ?? t`Item Updated`, + successMessage: + props.successMessage === null + ? null + : (props.successMessage ?? t`Item Updated`), method: 'PATCH' }), [props] @@ -120,7 +126,10 @@ export function useDeleteApiFormModal(props: ApiFormModalProps) { method: 'DELETE', submitText: t`Delete`, submitColor: 'red', - successMessage: props.successMessage ?? t`Item Deleted`, + successMessage: + props.successMessage === null + ? null + : (props.successMessage ?? t`Item Deleted`), preFormContent: props.preFormContent ?? ( - - - + + + ); }, [supplierPart, instanceQuery.isFetching]); diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index b6f026943b..552a7a5a64 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -212,6 +212,7 @@ export default function StockDetail() { name: 'supplier_part', label: t`Supplier Part`, type: 'link', + model_field: 'SKU', model: ModelType.supplierpart, hidden: !stockitem.supplier_part }, diff --git a/src/frontend/tests/pui_printing.spec.ts b/src/frontend/tests/pui_printing.spec.ts index 04c762416c..917ee149b3 100644 --- a/src/frontend/tests/pui_printing.spec.ts +++ b/src/frontend/tests/pui_printing.spec.ts @@ -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 }).click(); - await page.locator('#form-success').waitFor(); - await page.getByText('Label printing completed').waitFor(); - + await page.getByText('Printing completed successfully').first().waitFor(); await page.context().close(); }); @@ -75,12 +73,10 @@ test('Report Printing', async ({ page }) => { await page.waitForTimeout(100); // Submit the print form (should result in success) - await page.getByRole('button', { name: 'Generate', exact: true }).isEnabled(); - await page.getByRole('button', { name: 'Generate', exact: true }).click(); - - await page.locator('#form-success').waitFor(); - await page.getByText('Report printing completed').waitFor(); + await page.getByRole('button', { name: 'Print', exact: true }).isEnabled(); + await page.getByRole('button', { name: 'Print', exact: true }).click(); + await page.getByText('Printing completed successfully').first().waitFor(); await page.context().close(); });