mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 12:05:53 +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:
@ -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();
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user