Support ReturnOrder

This commit is contained in:
Oliver Walters 2026-06-30 14:01:07 +00:00
parent b28b1f7330
commit ed074810a4
4 changed files with 307 additions and 234 deletions

View File

@ -2,6 +2,7 @@ import { ModelType } from '@lib/enums/ModelType';
import type { ReactNode } from 'react';
import { PartPreviewComponent } from './models/PartPreview';
import { PurchaseOrderPreviewComponent } from './models/PurchaseOrderPreview';
import { ReturnOrderPreviewComponent } from './models/ReturnOrderPreview';
import { SalesOrderPreviewComponent } from './models/SalesOrderPreview';
import { StockPreviewComponent } from './models/StockPreview';
@ -34,6 +35,8 @@ export function getPreviewComponentForModel({
return PurchaseOrderPreviewComponent({ instance, modelId });
case ModelType.salesorder:
return SalesOrderPreviewComponent({ instance, modelId });
case ModelType.returnorder:
return ReturnOrderPreviewComponent({ instance, modelId });
default:
return null;
}

View File

@ -0,0 +1,25 @@
import { t } from '@lingui/core/macro';
import { ReturnOrderDetailsPanel } from '../../../pages/sales/ReturnOrderDetailsPanel';
import type { PreviewType } from '../PreviewType';
export function ReturnOrderPreviewComponent({
instance,
modelId
}: Readonly<{
instance: any;
modelId: number;
}>): PreviewType {
const customer = instance?.customer_detail?.name ?? instance?.customer_name;
const ref = instance?.reference ?? `#${modelId}`;
let title = `${t`Return Order`} ${ref}`;
if (customer) {
title += ` (${customer})`;
}
return {
title,
preview: <ReturnOrderDetailsPanel instance={instance} />
};
}

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/core/macro';
import { Accordion, Grid, Skeleton, Stack, Text } from '@mantine/core';
import { Accordion, Stack } from '@mantine/core';
import { IconInfoCircle, IconList } from '@tabler/icons-react';
import { type ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom';
@ -9,17 +9,10 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import { TagsList } from '@lib/index';
import type { PanelType } from '@lib/types/Panel';
import AdminButton from '../../components/buttons/AdminButton';
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
import { PrintingActions } from '../../components/buttons/PrintingActions';
import {
type DetailsField,
DetailsTable
} from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
BarcodeActionDropdown,
CancelItemAction,
@ -34,9 +27,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
import NotesPanel from '../../components/panels/NotesPanel';
import { PanelGroup } from '../../components/panels/PanelGroup';
import ParametersPanel from '../../components/panels/ParametersPanel';
import { RenderAddress } from '../../components/render/Company';
import { StatusRenderer } from '../../components/render/StatusRenderer';
import { formatCurrency } from '../../defaults/formatters';
import { useReturnOrderFields } from '../../forms/ReturnOrderForms';
import {
useCreateApiFormModal,
@ -48,6 +39,7 @@ import { useGlobalSettingsState } from '../../states/SettingsStates';
import { useUserState } from '../../states/UserState';
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
import ReturnOrderLineItemTable from '../../tables/sales/ReturnOrderLineItemTable';
import { ReturnOrderDetailsPanel } from './ReturnOrderDetailsPanel';
/**
* Detail page for a single ReturnOrder
@ -99,236 +91,19 @@ export default function ReturnOrderDetail() {
);
}, [order, globalSettings]);
const detailsPanel = useMemo(() => {
if (instanceQuery.isFetching) {
return <Skeleton />;
}
const tl: DetailsField[] = [
{
type: 'text',
name: 'reference',
label: t`Reference`,
copy: true
},
{
type: 'text',
name: 'customer_reference',
label: t`Customer Reference`,
icon: 'customer',
copy: true,
hidden: !order.customer_reference
},
{
type: 'link',
name: 'customer',
icon: 'customers',
label: t`Customer`,
model: ModelType.company
},
{
type: 'text',
name: 'description',
label: t`Description`,
copy: true
},
{
type: 'status',
name: 'status',
label: t`Status`,
model: ModelType.returnorder
},
{
type: 'status',
name: 'status_custom_key',
label: t`Custom Status`,
model: ModelType.returnorder,
icon: 'status',
hidden:
!order.status_custom_key || order.status_custom_key == order.status
}
];
const tr: DetailsField[] = [
{
type: 'text',
name: 'line_items',
label: t`Line Items`,
icon: 'list'
},
{
type: 'progressbar',
name: 'completed',
icon: 'progress',
label: t`Completed Line Items`,
total: order.line_items,
progress: order.completed_lines
},
{
type: 'text',
name: 'currency',
label: t`Order Currency`,
value_formatter: () =>
order?.order_currency ?? order?.customer_detail?.currency
},
{
type: 'text',
name: 'total_price',
label: t`Total Cost`,
value_formatter: () => {
return formatCurrency(order?.total_price, {
currency: order?.order_currency || order?.customer_detail?.currency
});
}
}
];
const bl: DetailsField[] = [
{
type: 'link',
external: true,
name: 'link',
label: t`Link`,
copy: true,
hidden: !order.link
},
{
type: 'text',
name: 'address',
label: t`Return Address`,
icon: 'address',
value_formatter: () =>
order.address_detail ? (
<RenderAddress instance={order.address_detail} />
) : (
<Text size='sm' c='red'>{t`Not specified`}</Text>
)
},
{
type: 'text',
name: 'contact_detail.name',
label: t`Contact`,
icon: 'user',
copy: true,
hidden: !order.contact
},
{
type: 'text',
name: 'contact_detail.email',
label: t`Contact Email`,
icon: 'email',
copy: true,
hidden: !order.contact_detail?.email
},
{
type: 'text',
name: 'contact_detail.phone',
label: t`Contact Phone`,
icon: 'phone',
copy: true,
hidden: !order.contact_detail?.phone
},
{
type: 'text',
name: 'project_code_label',
label: t`Project Code`,
icon: 'reference',
copy: true,
hidden: !order.project_code
},
{
type: 'text',
name: 'responsible',
label: t`Responsible`,
badge: 'owner',
hidden: !order.responsible
}
];
const br: DetailsField[] = [
{
type: 'date',
name: 'creation_date',
label: t`Creation Date`,
icon: 'calendar',
copy: true,
hidden: !order.creation_date
},
{
type: 'date',
name: 'issue_date',
label: t`Issue Date`,
icon: 'calendar',
copy: true,
hidden: !order.issue_date
},
{
type: 'date',
name: 'start_date',
label: t`Start Date`,
icon: 'calendar',
copy: true,
hidden: !order.start_date
},
{
type: 'date',
name: 'target_date',
label: t`Target Date`,
copy: true,
hidden: !order.target_date
},
{
type: 'date',
name: 'complete_date',
icon: 'calendar_check',
label: t`Completion Date`,
copy: true,
hidden: !order.complete_date
},
{
type: 'date',
name: 'updated_at',
label: t`Last Updated`,
icon: 'calendar',
copy: true,
showTime: true,
hidden: !order.updated_at
}
];
return (
<ItemDetailsGrid
tables={[
{ fields: tr, item: order },
{ fields: bl, item: order },
{ fields: br, item: order }
]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
appRole={UserRoles.purchase_order}
apiPath={ApiEndpoints.company_list}
src={order.customer_detail?.image}
pk={order.customer}
/>
<Grid.Col span={{ base: 12, sm: 8 }}>
<DetailsTable fields={tl} item={order} />
</Grid.Col>
</Grid>
<TagsList tags={order.tags} />
</Stack>
</ItemDetailsGrid>
);
}, [order, instanceQuery]);
const orderPanels: PanelType[] = useMemo(() => {
return [
{
name: 'detail',
label: t`Order Details`,
icon: <IconInfoCircle />,
content: detailsPanel
content: (
<ReturnOrderDetailsPanel
instance={order}
allowImageEdit
refreshInstance={refreshInstance}
/>
)
},
{
name: 'line-items',

View File

@ -0,0 +1,270 @@
import { t } from '@lingui/core/macro';
import { Grid, Skeleton, Stack, Text } from '@mantine/core';
import { useMemo } from 'react';
import TagsList from '@lib/components/TagsList';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import {
type DetailsField,
DetailsTable
} from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
import { RenderAddress } from '../../components/render/Company';
import { formatCurrency } from '../../defaults/formatters';
import { useGlobalSettingsState } from '../../states/SettingsStates';
export function ReturnOrderDetailsPanel({
instance,
allowImageEdit = false,
refreshInstance
}: Readonly<{
instance: any;
allowImageEdit?: boolean;
refreshInstance?: () => void;
}>) {
const globalSettings = useGlobalSettingsState();
const orderCurrency = useMemo(
() =>
instance?.order_currency ||
instance?.customer_detail?.currency ||
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY'),
[instance, globalSettings]
);
const tl: DetailsField[] = [
{
type: 'text',
name: 'reference',
label: t`Reference`,
copy: true
},
{
type: 'text',
name: 'customer_reference',
label: t`Customer Reference`,
icon: 'customer',
copy: true,
hidden: !instance?.customer_reference
},
{
type: 'link',
name: 'customer',
icon: 'customers',
label: t`Customer`,
model: ModelType.company
},
{
type: 'text',
name: 'description',
label: t`Description`,
copy: true
},
{
type: 'status',
name: 'status',
label: t`Status`,
model: ModelType.returnorder
},
{
type: 'status',
name: 'status_custom_key',
label: t`Custom Status`,
model: ModelType.returnorder,
icon: 'status',
hidden:
!instance?.status_custom_key ||
instance?.status_custom_key == instance?.status
}
];
const tr: DetailsField[] = [
{
type: 'text',
name: 'line_items',
label: t`Line Items`,
icon: 'list'
},
{
type: 'progressbar',
name: 'completed',
icon: 'progress',
label: t`Completed Line Items`,
total: instance?.line_items,
progress: instance?.completed_lines
},
{
type: 'text',
name: 'currency',
label: t`Order Currency`,
value_formatter: () =>
instance?.order_currency ?? instance?.customer_detail?.currency
},
{
type: 'text',
name: 'total_price',
label: t`Total Cost`,
value_formatter: () =>
formatCurrency(instance?.total_price, {
currency:
instance?.order_currency || instance?.customer_detail?.currency
})
}
];
const bl: DetailsField[] = [
{
type: 'link',
external: true,
name: 'link',
label: t`Link`,
copy: true,
hidden: !instance?.link
},
{
type: 'text',
name: 'address',
label: t`Return Address`,
icon: 'address',
value_formatter: () =>
instance?.address_detail ? (
<RenderAddress instance={instance.address_detail} />
) : (
<Text size='sm' c='red'>{t`Not specified`}</Text>
)
},
{
type: 'text',
name: 'contact_detail.name',
label: t`Contact`,
icon: 'user',
copy: true,
hidden: !instance?.contact
},
{
type: 'text',
name: 'contact_detail.email',
label: t`Contact Email`,
icon: 'email',
copy: true,
hidden: !instance?.contact_detail?.email
},
{
type: 'text',
name: 'contact_detail.phone',
label: t`Contact Phone`,
icon: 'phone',
copy: true,
hidden: !instance?.contact_detail?.phone
},
{
type: 'text',
name: 'project_code_label',
label: t`Project Code`,
icon: 'reference',
copy: true,
hidden: !instance?.project_code
},
{
type: 'text',
name: 'responsible',
label: t`Responsible`,
badge: 'owner',
hidden: !instance?.responsible
}
];
const br: DetailsField[] = [
{
type: 'date',
name: 'creation_date',
label: t`Creation Date`,
icon: 'calendar',
copy: true,
hidden: !instance?.creation_date
},
{
type: 'date',
name: 'issue_date',
label: t`Issue Date`,
icon: 'calendar',
copy: true,
hidden: !instance?.issue_date
},
{
type: 'date',
name: 'start_date',
label: t`Start Date`,
icon: 'calendar',
copy: true,
hidden: !instance?.start_date
},
{
type: 'date',
name: 'target_date',
label: t`Target Date`,
copy: true,
hidden: !instance?.target_date
},
{
type: 'date',
name: 'complete_date',
icon: 'calendar_check',
label: t`Completion Date`,
copy: true,
hidden: !instance?.complete_date
},
{
type: 'date',
name: 'updated_at',
label: t`Last Updated`,
icon: 'calendar',
copy: true,
showTime: true,
hidden: !instance?.updated_at
}
];
const parametersTable = useParameterDetailsGrid({
model_type: ModelType.returnorder,
model_id: instance?.pk
});
if (!instance?.pk) return <Skeleton />;
return (
<ItemDetailsGrid
tables={[
{ fields: tr, item: instance },
{ fields: bl, item: instance },
{ fields: br, item: instance },
parametersTable
]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
appRole={allowImageEdit ? UserRoles.return_order : undefined}
imageActions={
allowImageEdit ? { uploadFile: true, deleteFile: true } : {}
}
apiPath={apiUrl(ApiEndpoints.company_list, instance?.customer)}
src={instance?.customer_detail?.image}
pk={instance?.customer}
refresh={refreshInstance}
/>
<Grid.Col span={{ base: 12, sm: 8 }}>
<DetailsTable fields={tl} item={instance} />
</Grid.Col>
</Grid>
<TagsList tags={instance?.tags} />
</Stack>
</ItemDetailsGrid>
);
}