2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-30 12:45:42 +00:00

[UI] Implement "checked_by" for SalesOrderShipment (#10654)

* Add "checked" column to SalesOrderStatus table

* Add API filter for "checked" status

* Add Checked / Not Checked badge

* Add actions to check / uncheck shipment

* Add modal for changing checked_by status

* Display checked_by user

* Tweak wording

* Bump API version

* Update CHANGELOG file

* Update docs

* Add new global setting

- Prevent shipment completion which have not been checked

* Test if shipment has been checked

* Updated unit tests

* Updated type hinting

(may as well while I'm here)

* Adjust shipment icon

* Add "order_outstanding" filter for SalesOrderShipment table
This commit is contained in:
Oliver
2025-10-24 13:39:57 +11:00
committed by GitHub
parent 435d34568b
commit 6df97e83f5
14 changed files with 295 additions and 76 deletions

View File

@@ -31,6 +31,7 @@ import {
IconCornerDownLeft,
IconCornerDownRight,
IconCornerUpRightDouble,
IconCubeSend,
IconCurrencyDollar,
IconDots,
IconEdit,
@@ -154,7 +155,7 @@ const icons: InvenTreeIconType = {
sales_orders: IconTruckDelivery,
scheduling: IconCalendarStats,
scrap: IconCircleX,
shipment: IconTruckDelivery,
shipment: IconCubeSend,
test_templates: IconTestPipe,
test: IconTestPipe,
related_parts: IconLayersLinked,

View File

@@ -305,7 +305,8 @@ export default function SystemSettings() {
'SALESORDER_REQUIRE_RESPONSIBLE',
'SALESORDER_DEFAULT_SHIPMENT',
'SALESORDER_EDIT_COMPLETED_ORDERS',
'SALESORDER_SHIP_COMPLETE'
'SALESORDER_SHIP_COMPLETE',
'SALESORDER_SHIPMENT_REQUIRES_CHECK'
]}
/>
)

View File

@@ -2,10 +2,10 @@ import { t } from '@lingui/core/macro';
import { Accordion, Grid, Skeleton, Stack, Text } from '@mantine/core';
import {
IconBookmark,
IconCubeSend,
IconInfoCircle,
IconList,
IconTools,
IconTruckDelivery
IconTools
} from '@tabler/icons-react';
import { type ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom';
@@ -394,7 +394,7 @@ export default function SalesOrderDetail() {
{
name: 'shipments',
label: t`Shipments`,
icon: <IconTruckDelivery />,
icon: <IconCubeSend />,
content: (
<SalesOrderShipmentTable
orderId={order.pk}

View File

@@ -1,6 +1,11 @@
import { t } from '@lingui/core/macro';
import { Grid, Skeleton, Stack, Text } from '@mantine/core';
import { IconBookmark, IconInfoCircle } from '@tabler/icons-react';
import { Alert, Grid, Skeleton, Stack, Text } from '@mantine/core';
import {
IconBookmark,
IconCircleCheck,
IconCircleX,
IconInfoCircle
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
@@ -31,6 +36,7 @@ import NotesPanel from '../../components/panels/NotesPanel';
import type { PanelType } from '../../components/panels/Panel';
import { PanelGroup } from '../../components/panels/PanelGroup';
import { RenderAddress } from '../../components/render/Company';
import { RenderUser } from '../../components/render/User';
import { formatDate } from '../../defaults/formatters';
import {
useSalesOrderShipmentCompleteFields,
@@ -50,6 +56,8 @@ export default function SalesOrderShipmentDetail() {
const user = useUserState();
const navigate = useNavigate();
const userId = useMemo(() => user.userId(), [user]);
const {
instance: shipment,
instanceQuery: shipmentQuery,
@@ -74,6 +82,8 @@ export default function SalesOrderShipmentDetail() {
const isPending = useMemo(() => !shipment.shipment_date, [shipment]);
const isChecked = useMemo(() => !!shipment.checked_by, [shipment]);
const detailsPanel = useMemo(() => {
if (shipmentQuery.isFetching || customerQuery.isFetching) {
return <Skeleton />;
@@ -180,6 +190,18 @@ export default function SalesOrderShipmentDetail() {
icon: 'packages',
label: t`Allocated Items`
},
{
type: 'text',
name: 'checked_by',
label: t`Checked By`,
icon: 'check',
value_formatter: () =>
shipment.checked_by_detail ? (
<RenderUser instance={shipment.checked_by_detail} />
) : (
<Text size='sm' c='red'>{t`Not checked`}</Text>
)
},
{
type: 'text',
name: 'shipment_date',
@@ -298,6 +320,46 @@ export default function SalesOrderShipmentDetail() {
onFormSuccess: refreshShipment
});
const checkShipment = useEditApiFormModal({
url: ApiEndpoints.sales_order_shipment_list,
pk: shipment.pk,
title: t`Check Shipment`,
preFormContent: (
<Alert color='green' icon={<IconCircleCheck />} title={t`Check Shipment`}>
<Text>{t`Marking the shipment as checked indicates that you have verified that all items included in this shipment are correct`}</Text>
</Alert>
),
fetchInitialData: false,
fields: {
checked_by: {
hidden: true,
value: userId
}
},
successMessage: t`Shipment marked as checked`,
onFormSuccess: refreshShipment
});
const uncheckShipment = useEditApiFormModal({
url: ApiEndpoints.sales_order_shipment_list,
pk: shipment.pk,
title: t`Uncheck Shipment`,
preFormContent: (
<Alert color='red' icon={<IconCircleX />} title={t`Uncheck Shipment`}>
<Text>{t`Marking the shipment as unchecked indicates that the shipment requires further verification`}</Text>
</Alert>
),
fetchInitialData: false,
fields: {
checked_by: {
hidden: true,
value: null
}
},
successMessage: t`Shipment marked as unchecked`,
onFormSuccess: refreshShipment
});
const shipmentBadges = useMemo(() => {
if (shipmentQuery.isFetching) {
return [];
@@ -310,6 +372,18 @@ export default function SalesOrderShipmentDetail() {
color='gray'
visible={isPending}
/>,
<DetailsBadge
key='checked'
label={t`Checked`}
color='green'
visible={isPending && isChecked}
/>,
<DetailsBadge
key='not-checked'
label={t`Not Checked`}
color='red'
visible={isPending && !isChecked}
/>,
<DetailsBadge
key='shipped'
label={t`Shipped`}
@@ -323,7 +397,7 @@ export default function SalesOrderShipmentDetail() {
visible={!!shipment.delivery_date}
/>
];
}, [isPending, shipment.deliveryDate, shipmentQuery.isFetching]);
}, [isPending, isChecked, shipment.deliveryDate, shipmentQuery.isFetching]);
const shipmentActions = useMemo(() => {
const canEdit: boolean = user.hasChangePermission(
@@ -363,6 +437,20 @@ export default function SalesOrderShipmentDetail() {
onClick: editShipment.open,
tooltip: t`Edit Shipment`
}),
{
hidden: !isPending || isChecked,
name: t`Check`,
tooltip: t`Mark shipment as checked`,
icon: <IconCircleCheck color='green' />,
onClick: checkShipment.open
},
{
hidden: !isPending || !isChecked,
name: t`Uncheck`,
tooltip: t`Mark shipment as unchecked`,
icon: <IconCircleX color='red' />,
onClick: uncheckShipment.open
},
CancelItemAction({
hidden: !isPending,
onClick: deleteShipment.open,
@@ -371,13 +459,15 @@ export default function SalesOrderShipmentDetail() {
]}
/>
];
}, [isPending, user, shipment]);
}, [isChecked, isPending, user, shipment]);
return (
<>
{completeShipment.modal}
{editShipment.modal}
{deleteShipment.modal}
{checkShipment.modal}
{uncheckShipment.modal}
<InstanceDetail
query={shipmentQuery}
requiredRole={UserRoles.sales_order}

View File

@@ -107,6 +107,13 @@ export default function SalesOrderShipmentTable({
switchable: false,
title: t`Items`
},
{
accessor: 'checked',
title: t`Checked`,
switchable: true,
sortable: false,
render: (record: any) => <YesNoButton value={!!record.checked_by} />
},
{
accessor: 'shipped',
title: t`Shipped`,
@@ -114,6 +121,13 @@ export default function SalesOrderShipmentTable({
sortable: false,
render: (record: any) => <YesNoButton value={!!record.shipment_date} />
},
{
accessor: 'delivered',
title: t`Delivered`,
switchable: true,
sortable: false,
render: (record: any) => <YesNoButton value={!!record.delivery_date} />
},
DateColumn({
accessor: 'shipment_date',
title: t`Shipment Date`
@@ -191,6 +205,11 @@ export default function SalesOrderShipmentTable({
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'checked',
label: t`Checked`,
description: t`Show shipments which have been checked`
},
{
name: 'shipped',
label: t`Shipped`,