2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-13 18:43:08 +00:00

[WIP] Background reports ()

* 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:
Oliver 2025-03-04 23:40:54 +11:00 committed by GitHub
parent d5a176c121
commit d822b9b574
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 407 additions and 130 deletions
.github/workflows
src

@ -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:

@ -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

@ -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)

@ -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."""

@ -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()

@ -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(
'<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
path(
'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
path(

@ -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()

@ -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

@ -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)

@ -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<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({
items,
@ -46,6 +139,21 @@ export function PrintingActions({
return enableReports && globalSettings.isSet('REPORT_ENABLE');
}, [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
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);
}
});

@ -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;

@ -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 ?? (
<Alert
color={'red'}

@ -122,7 +122,7 @@ export default function SupplierPartDetail() {
}
];
const tr: DetailsField[] = [
const bl: DetailsField[] = [
{
type: 'link',
name: 'supplier',
@ -165,7 +165,7 @@ export default function SupplierPartDetail() {
}
];
const bl: DetailsField[] = [
const br: DetailsField[] = [
{
type: 'string',
name: 'packaging',
@ -183,7 +183,7 @@ export default function SupplierPartDetail() {
}
];
const br: DetailsField[] = [
const tr: DetailsField[] = [
{
type: 'string',
name: 'in_stock',
@ -232,9 +232,9 @@ export default function SupplierPartDetail() {
<DetailsTable title={t`Part Details`} fields={tl} item={data} />
</Grid.Col>
</Grid>
<DetailsTable title={t`Supplier`} fields={tr} item={data} />
<DetailsTable title={t`Packaging`} fields={bl} item={data} />
<DetailsTable title={t`Availability`} fields={br} item={data} />
<DetailsTable title={t`Supplier`} fields={bl} item={data} />
<DetailsTable title={t`Packaging`} fields={br} item={data} />
<DetailsTable title={t`Availability`} fields={tr} item={data} />
</ItemDetailsGrid>
);
}, [supplierPart, instanceQuery.isFetching]);

@ -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
},

@ -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();
});