2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 12:35:46 +00:00

[PUI] Sales order shipments (#8250)

* Refactor AttachmentPanel into common component

* Remove unused imports

* Add very basic implementation for SalesOrderShipmentDetail page

* Refactor NotesPanel into common component

* Fetch customer data

* Add some placeholder actions

* Updates for shipment detail page

* Adjust SalesOrderShipment API

* Add badges

* Implement API filter for SalesOrderAllocation

* Display allocation table on shipment page

* Add placeholder action to edit allocations

* Improvements for SalesOrderAllocationTable

* Improve API db fetch efficiency

* Edit / delete pending allocations

* Fix for legacy CUI tables

* API tweaks

* Revert custom attachment code for SalesOrderShipment

* Implement "complete shipment" form

* Allocate stock item(s) to sales order

* Fixes for TableField rendering

* Reset sourceLocation when form opens

* Updated playwrigh tests

* Tweak branch (will be reverted)

* Revert github workflow
This commit is contained in:
Oliver
2024-10-10 22:43:22 +11:00
committed by GitHub
parent 35969b11a5
commit 33eba14d3f
44 changed files with 1370 additions and 536 deletions

View File

@ -0,0 +1,366 @@
import { t } from '@lingui/macro';
import { Grid, Skeleton, Stack } from '@mantine/core';
import { IconInfoCircle, IconPackages } from '@tabler/icons-react';
import { useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
import { PrintingActions } from '../../components/buttons/PrintingActions';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge';
import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
BarcodeActionDropdown,
CancelItemAction,
EditItemAction,
OptionsActionDropdown
} from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
import AttachmentPanel from '../../components/panels/AttachmentPanel';
import NotesPanel from '../../components/panels/NotesPanel';
import { PanelType } from '../../components/panels/Panel';
import { PanelGroup } from '../../components/panels/PanelGroup';
import { formatDate } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import {
useSalesOrderShipmentCompleteFields,
useSalesOrderShipmentFields
} from '../../forms/SalesOrderForms';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { useUserState } from '../../states/UserState';
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
export default function SalesOrderShipmentDetail() {
const { id } = useParams();
const user = useUserState();
const navigate = useNavigate();
const {
instance: shipment,
instanceQuery: shipmentQuery,
refreshInstance: refreshShipment,
requestStatus: shipmentStatus
} = useInstance({
endpoint: ApiEndpoints.sales_order_shipment_list,
pk: id,
params: {
order_detail: true
}
});
const {
instance: customer,
instanceQuery: customerQuery,
refreshInstance: refreshCustomer,
requestStatus: customerStatus
} = useInstance({
endpoint: ApiEndpoints.company_list,
pk: shipment.order_detail?.customer,
hasPrimaryKey: true
});
const isPending = useMemo(() => !shipment.shipment_date, [shipment]);
const detailsPanel = useMemo(() => {
if (shipmentQuery.isFetching || customerQuery.isFetching) {
return <Skeleton />;
}
let data: any = {
...shipment,
customer: customer?.pk,
customer_name: customer?.name,
customer_reference: shipment.order_detail?.customer_reference
};
// Top Left: Order / customer information
let tl: DetailsField[] = [
{
type: 'link',
model: ModelType.salesorder,
name: 'order',
label: t`Sales Order`,
icon: 'sales_orders',
model_field: 'reference'
},
{
type: 'link',
name: 'customer',
icon: 'customers',
label: t`Customer`,
model: ModelType.company,
model_field: 'name',
hidden: !data.customer
},
{
type: 'text',
name: 'customer_reference',
icon: 'serial',
label: t`Customer Reference`,
hidden: !data.customer_reference,
copy: true
},
{
type: 'text',
name: 'reference',
icon: 'serial',
label: t`Shipment Reference`,
copy: true
},
{
type: 'text',
name: 'allocated_items',
icon: 'packages',
label: t`Allocated Items`
}
];
// Top right: Shipment information
let tr: DetailsField[] = [
{
type: 'text',
name: 'tracking_number',
label: t`Tracking Number`,
icon: 'trackable',
value_formatter: () => shipment.tracking_number || '---',
copy: !!shipment.tracking_number
},
{
type: 'text',
name: 'invoice_number',
label: t`Invoice Number`,
icon: 'serial',
value_formatter: () => shipment.invoice_number || '---',
copy: !!shipment.invoice_number
},
{
type: 'text',
name: 'shipment_date',
label: t`Shipment Date`,
icon: 'calendar',
value_formatter: () => formatDate(shipment.shipment_date),
hidden: !shipment.shipment_date
},
{
type: 'text',
name: 'delivery_date',
label: t`Delivery Date`,
icon: 'calendar',
value_formatter: () => formatDate(shipment.delivery_date),
hidden: !shipment.delivery_date
},
{
type: 'link',
external: true,
name: 'link',
label: t`Link`,
copy: true,
hidden: !shipment.link
}
];
return (
<>
<ItemDetailsGrid>
<Grid>
<Grid.Col span={4}>
<DetailsImage
appRole={UserRoles.sales_order}
apiPath={ApiEndpoints.company_list}
src={customer?.image}
pk={customer?.pk}
imageActions={{
selectExisting: false,
downloadImage: false,
uploadFile: false,
deleteFile: false
}}
/>
</Grid.Col>
<Grid.Col span={8}>
<DetailsTable fields={tl} item={data} />
</Grid.Col>
</Grid>
<DetailsTable fields={tr} item={data} />
</ItemDetailsGrid>
</>
);
}, [shipment, shipmentQuery, customer, customerQuery]);
const shipmentPanels: PanelType[] = useMemo(() => {
return [
{
name: 'detail',
label: t`Shipment Details`,
icon: <IconInfoCircle />,
content: detailsPanel
},
{
name: 'items',
label: t`Assigned Items`,
icon: <IconPackages />,
content: (
<SalesOrderAllocationTable
shipmentId={shipment.pk}
showPartInfo
allowEdit={isPending}
modelField="item"
modelTarget={ModelType.stockitem}
/>
)
},
AttachmentPanel({
model_type: ModelType.salesordershipment,
model_id: shipment.pk
}),
NotesPanel({
model_type: ModelType.salesordershipment,
model_id: shipment.pk
})
];
}, [isPending, shipment, detailsPanel]);
const editShipmentFields = useSalesOrderShipmentFields({
pending: isPending
});
const editShipment = useEditApiFormModal({
url: ApiEndpoints.sales_order_shipment_list,
pk: shipment.pk,
fields: editShipmentFields,
title: t`Edit Shipment`,
onFormSuccess: refreshShipment
});
const deleteShipment = useDeleteApiFormModal({
url: ApiEndpoints.sales_order_shipment_list,
pk: shipment.pk,
title: t`Cancel Shipment`,
onFormSuccess: () => {
// Shipment has been deleted - navigate back to the sales order
navigate(getDetailUrl(ModelType.salesorder, shipment.order));
}
});
const completeShipmentFields = useSalesOrderShipmentCompleteFields({});
const completeShipment = useCreateApiFormModal({
url: ApiEndpoints.sales_order_shipment_complete,
pk: shipment.pk,
fields: completeShipmentFields,
title: t`Complete Shipment`,
focus: 'tracking_number',
initialData: {
...shipment,
shipment_date: new Date().toISOString().split('T')[0]
},
onFormSuccess: refreshShipment
});
const shipmentBadges = useMemo(() => {
if (shipmentQuery.isFetching) {
return [];
}
return [
<DetailsBadge label={t`Pending`} color="gray" visible={isPending} />,
<DetailsBadge label={t`Shipped`} color="green" visible={!isPending} />,
<DetailsBadge
label={t`Delivered`}
color="blue"
visible={!!shipment.delivery_date}
/>
];
}, [shipment, shipmentQuery]);
const shipmentActions = useMemo(() => {
const canEdit: boolean = user.hasChangePermission(
ModelType.salesordershipment
);
return [
<PrimaryActionButton
title={t`Send Shipment`}
icon="sales_orders"
hidden={!isPending}
color="green"
onClick={() => {
completeShipment.open();
}}
/>,
<BarcodeActionDropdown
model={ModelType.salesordershipment}
pk={shipment.pk}
/>,
<PrintingActions
modelType={ModelType.salesordershipment}
items={[shipment.pk]}
enableLabels
enableReports
/>,
<OptionsActionDropdown
tooltip={t`Shipment Actions`}
actions={[
EditItemAction({
hidden: !canEdit,
onClick: editShipment.open,
tooltip: t`Edit Shipment`
}),
CancelItemAction({
hidden: !isPending,
onClick: deleteShipment.open,
tooltip: t`Cancel Shipment`
})
]}
/>
];
}, [isPending, user, shipment]);
return (
<>
{completeShipment.modal}
{editShipment.modal}
{deleteShipment.modal}
<InstanceDetail
status={shipmentStatus}
loading={shipmentQuery.isFetching || customerQuery.isFetching}
>
<Stack gap="xs">
<PageDetail
title={t`Sales Order Shipment` + `: ${shipment.reference}`}
subtitle={t`Sales Order` + `: ${shipment.order_detail?.reference}`}
breadcrumbs={[
{ name: t`Sales`, url: '/sales/' },
{
name: shipment.order_detail?.reference,
url: getDetailUrl(ModelType.salesorder, shipment.order)
}
]}
badges={shipmentBadges}
imageUrl={customer?.image}
editAction={editShipment.open}
editEnabled={user.hasChangePermission(ModelType.salesordershipment)}
actions={shipmentActions}
/>
<PanelGroup
pageKey="salesordershipment"
panels={shipmentPanels}
model={ModelType.salesordershipment}
instance={shipment}
id={shipment.pk}
/>
</Stack>
</InstanceDetail>
</>
);
}