mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-04 10:31:03 +00:00
Fix complete_sales_order_shipment task (#11525)
* Fix complete_sales_order_shipment task - Perform allocation *before* marking shipment as complete - Ensure task is not marked as complete before it is actually done * Add unit test * Provide task status tracking for shipment completion * Add integration testing * Address unit test issues * Bump API version * Enhance playwright test
This commit is contained in:
@@ -24,7 +24,9 @@ import type {
|
||||
ApiFormFieldSet,
|
||||
ApiFormFieldType
|
||||
} from '@lib/types/Forms';
|
||||
import dayjs from 'dayjs';
|
||||
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
|
||||
import useBackgroundTask from '../hooks/UseBackgroundTask';
|
||||
import { useCreateApiFormModal, useEditApiFormModal } from '../hooks/UseForm';
|
||||
import { useGlobalSettingsState } from '../states/SettingsStates';
|
||||
import { useUserState } from '../states/UserState';
|
||||
@@ -254,6 +256,45 @@ export function useUncheckShipmentForm({
|
||||
});
|
||||
}
|
||||
|
||||
export function useCompleteShipmentForm({
|
||||
shipment,
|
||||
onSuccess
|
||||
}: {
|
||||
shipment: any;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const [taskId, setTaskId] = useState<string>('');
|
||||
|
||||
const completeShipmentFields = useSalesOrderShipmentCompleteFields({});
|
||||
|
||||
useBackgroundTask({
|
||||
taskId: taskId,
|
||||
message: t`Completing shipment`,
|
||||
successMessage: t`Shipment completed successfully`,
|
||||
onSuccess: onSuccess
|
||||
});
|
||||
|
||||
return useCreateApiFormModal({
|
||||
url: ApiEndpoints.sales_order_shipment_complete,
|
||||
pk: shipment.pk,
|
||||
title: t`Complete Shipment`,
|
||||
fields: completeShipmentFields,
|
||||
focus: 'tracking_number',
|
||||
initialData: {
|
||||
...shipment,
|
||||
shipment_date: dayjs().format('YYYY-MM-DD')
|
||||
},
|
||||
successMessage: null,
|
||||
onFormSuccess: (response: any) => {
|
||||
if (response.task_id) {
|
||||
setTaskId(response.task_id);
|
||||
} else {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function SalesOrderAllocateLineRow({
|
||||
props,
|
||||
record,
|
||||
@@ -405,6 +446,7 @@ export function useAllocateToSalesOrderForm({
|
||||
}
|
||||
},
|
||||
shipment: {
|
||||
autoFill: true,
|
||||
filters: {
|
||||
shipped: false,
|
||||
order_detail: true,
|
||||
|
||||
@@ -13,7 +13,6 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||
import dayjs from 'dayjs';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import {
|
||||
@@ -40,12 +39,11 @@ import { RenderUser } from '../../components/render/User';
|
||||
import { formatDate } from '../../defaults/formatters';
|
||||
import {
|
||||
useCheckShipmentForm,
|
||||
useSalesOrderShipmentCompleteFields,
|
||||
useCompleteShipmentForm,
|
||||
useSalesOrderShipmentFields,
|
||||
useUncheckShipmentForm
|
||||
} from '../../forms/SalesOrderForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
@@ -304,19 +302,9 @@ export default function SalesOrderShipmentDetail() {
|
||||
}
|
||||
});
|
||||
|
||||
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: dayjs().format('YYYY-MM-DD')
|
||||
},
|
||||
onFormSuccess: refreshShipment
|
||||
const completeShipment = useCompleteShipmentForm({
|
||||
shipment: shipment,
|
||||
onSuccess: refreshShipment
|
||||
});
|
||||
|
||||
const checkShipment = useCheckShipmentForm({
|
||||
|
||||
@@ -292,6 +292,7 @@ export default function SalesOrderLineItemTable({
|
||||
) : undefined,
|
||||
initialData: initialData,
|
||||
fields: allocateSerialFields,
|
||||
successMessage: t`Stock allocated successfully`,
|
||||
table: table
|
||||
});
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
useCheckShipmentForm,
|
||||
useCompleteShipmentForm,
|
||||
useSalesOrderShipmentCompleteFields,
|
||||
useSalesOrderShipmentFields,
|
||||
useUncheckShipmentForm
|
||||
@@ -77,6 +77,7 @@ export default function SalesOrderShipmentTable({
|
||||
url: ApiEndpoints.sales_order_shipment_list,
|
||||
fields: newShipmentFields,
|
||||
title: t`Create Shipment`,
|
||||
successMessage: t`Shipment created`,
|
||||
table: table,
|
||||
initialData: {
|
||||
order: orderId
|
||||
@@ -112,17 +113,9 @@ export default function SalesOrderShipmentTable({
|
||||
}
|
||||
});
|
||||
|
||||
const completeShipment = useCreateApiFormModal({
|
||||
url: ApiEndpoints.sales_order_shipment_complete,
|
||||
pk: selectedShipment.pk,
|
||||
fields: completeShipmentFields,
|
||||
title: t`Complete Shipment`,
|
||||
table: table,
|
||||
focus: 'tracking_number',
|
||||
initialData: {
|
||||
...selectedShipment,
|
||||
shipment_date: dayjs().format('YYYY-MM-DD')
|
||||
}
|
||||
const completeShipment = useCompleteShipmentForm({
|
||||
shipment: selectedShipment,
|
||||
onSuccess: table.refreshTable
|
||||
});
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
|
||||
@@ -417,7 +417,7 @@ export function StockItemTable({
|
||||
// Navigate to the first result
|
||||
navigate(getDetailUrl(ModelType.stockitem, response[0].pk));
|
||||
},
|
||||
successMessage: t`Stock item serialized`
|
||||
successMessage: t`Stock item created`
|
||||
});
|
||||
|
||||
const [partsToOrder, setPartsToOrder] = useState<any[]>([]);
|
||||
|
||||
@@ -262,19 +262,22 @@ test('Parts - Details', async ({ browser }) => {
|
||||
await page.getByText('Allocated to Sales Orders').waitFor();
|
||||
await page.getByText('Can Build').waitFor();
|
||||
|
||||
await page.getByText('0 / 10').waitFor();
|
||||
// The "allocated to sales order" quantity may vary, based on other tests
|
||||
await page.getByText(/0 \/ \d+/).waitFor();
|
||||
|
||||
// Depending on the state of other tests, the "In Production" value may vary
|
||||
// This could be either 4 / 49, or 5 / 49
|
||||
await page.getByText(/[4|5] \/ \d+/).waitFor();
|
||||
|
||||
// Badges
|
||||
await page.getByText('Required: 10').waitFor();
|
||||
await page.getByText(/Required: \d+/).waitFor();
|
||||
await page.getByText('No Stock').waitFor();
|
||||
await page.getByText(/In Production: [4|5]/).waitFor();
|
||||
|
||||
await page.getByText('Creation Date').waitFor();
|
||||
await page.getByText('2022-04-29').waitFor();
|
||||
|
||||
await page.getByText('Latest Serial Number').waitFor();
|
||||
});
|
||||
|
||||
test('Parts - Requirements', async ({ browser }) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
clickOnRowMenu,
|
||||
globalSearch,
|
||||
loadTab,
|
||||
navigate,
|
||||
setTableChoiceFilter,
|
||||
showCalendarView,
|
||||
showParametricView,
|
||||
@@ -238,6 +239,77 @@ test('Sales Orders - Shipments', async ({ browser }) => {
|
||||
.click();
|
||||
});
|
||||
|
||||
// Complete a shipment against a sales order
|
||||
test('Sales Orders - Complete Shipment', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
url: 'part/113/stock'
|
||||
});
|
||||
|
||||
const serialNumber = `SN${Math.floor(Math.random() * 100000)}`;
|
||||
const shipmentReference = `SHIP-${Math.floor(Math.random() * 100000)}`;
|
||||
|
||||
// First create some stock to allocate
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-add-stock-item' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('textbox', { name: 'text-field-serial_numbers' })
|
||||
.fill(serialNumber);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('Stock item created').first().waitFor();
|
||||
|
||||
// Navigate to the sales order and create a new shipment
|
||||
await navigate(page, '/sales/sales-order/7/shipments');
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-add-shipment' })
|
||||
.click();
|
||||
await page
|
||||
.getByLabel('text-field-reference', { exact: true })
|
||||
.fill(shipmentReference);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('Shipment created').first().waitFor();
|
||||
|
||||
// Back to the "line items" tab to allocate stock
|
||||
await loadTab(page, 'Line Items');
|
||||
const cell = await page.getByRole('cell', { name: 'MAST', exact: true });
|
||||
await clickOnRowMenu(cell);
|
||||
|
||||
// Allocate 1 item based on serial number
|
||||
await page.getByRole('menuitem', { name: 'Allocate serials' }).click();
|
||||
await page.getByRole('textbox', { name: 'number-field-quantity' }).fill('1');
|
||||
await page
|
||||
.getByRole('textbox', { name: 'text-field-serial_numbers' })
|
||||
.fill(serialNumber);
|
||||
await page.getByLabel('related-field-shipment').fill(shipmentReference);
|
||||
await page.getByText(`SO0007Shipment ${shipmentReference}`).click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('Stock allocated successfully').first().waitFor();
|
||||
|
||||
// Navigate to the shipment and mark it as "shipped"
|
||||
await loadTab(page, 'Shipments');
|
||||
await page.getByRole('cell', { name: shipmentReference }).click();
|
||||
await page.getByText(shipmentReference).first().waitFor();
|
||||
await page.getByText('Pending').first().waitFor();
|
||||
await loadTab(page, 'Allocated Stock');
|
||||
|
||||
// Check that the serial number is allocated as expected
|
||||
await page.getByRole('cell', { name: serialNumber }).waitFor();
|
||||
await page.getByRole('button', { name: 'Send Shipment' }).click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.getByText('Completing shipment').first().waitFor();
|
||||
await page.getByText('Shipment completed').first().waitFor();
|
||||
|
||||
await page.getByText('Shipped', { exact: true }).first().waitFor();
|
||||
|
||||
// Finally, navigate to the stock item and check it has been allocated to the customer
|
||||
await page.getByRole('cell', { name: serialNumber }).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByText('Unavailable').first().waitFor();
|
||||
await page.getByRole('link', { name: 'SO0007' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'Customer D' }).waitFor();
|
||||
});
|
||||
|
||||
test('Sales Orders - Duplicate', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
url: 'sales/sales-order/14/detail'
|
||||
|
||||
Reference in New Issue
Block a user