mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
Adds "ON HOLD" status to order models (#7807)
* Add "ON_HOLD" status code for orders * Add placeholder buttons for purchase order status change * Adds hooks for introspecting status code enumerations * Refactor status codes for import session - Remove hard-coded values * Refactor into <PrimaryActionButton /> * Cleanup * more permission checks * Add placeholder actions for SalesOrder * Placeholder actions for ReturnOrder * Placeholder actions for build order * Actions for "return order" * Update actions for return order - Add "on hold" transition * Implement transitions for SalesOrder * Allow control over SalesOrderLineItemTable * Implement PurchaseOrder actions * Improve API query lookup efficiency * UI cleanup * CUI cleanup * Build Order Updates - Implement StateTransitionMixin for BuildOrder model - Add BuildIssue API endpoint - Add BuildHold API endpoint - API query improvements - PUI actions * Increase timeout * Bump API version * Fix API version * Fix sales order actions * Update src/backend/InvenTree/order/serializers.py Co-authored-by: Matthias Mair <code@mjmair.com> * Adjust build filters * PUI updates * CUI refactoring for purchase orders * Refactor CUI sales order page * Refactor for return order * Refactor CUI build page * Playwright tests for build order * Add playwright test for sales orders * Add playwright test for purchase orders * js linting * Refactor return order page * Add missing functions from previous commit * Fix for "on order" badge on PartDetail page * UI tweaks * Fix unit tests * Update version check script * Fix typo * Enforce integer conversion for BaseEnum class * Unit test updates - Includes improvement for equality comparison for enums * Update documentation --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
41
src/frontend/src/components/buttons/PrimaryActionButton.tsx
Normal file
41
src/frontend/src/components/buttons/PrimaryActionButton.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { Button, Tooltip } from '@mantine/core';
|
||||
|
||||
import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons';
|
||||
import { notYetImplemented } from '../../functions/notifications';
|
||||
|
||||
/**
|
||||
* A "primary action" button for display on a page detail, (for example)
|
||||
*/
|
||||
export default function PrimaryActionButton({
|
||||
title,
|
||||
tooltip,
|
||||
icon,
|
||||
color,
|
||||
hidden,
|
||||
onClick
|
||||
}: {
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
icon?: InvenTreeIconType;
|
||||
color?: string;
|
||||
hidden?: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltip ?? title} position="bottom" hidden={!tooltip}>
|
||||
<Button
|
||||
leftSection={icon && <InvenTreeIcon icon={icon} />}
|
||||
color={color}
|
||||
radius="sm"
|
||||
p="xs"
|
||||
onClick={onClick ?? notYetImplemented}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
@ -368,6 +368,11 @@ export function ApiForm({
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not auto-focus on a 'choice' field
|
||||
if (field.field_type == 'choice') {
|
||||
return;
|
||||
}
|
||||
|
||||
focusField = fieldName;
|
||||
});
|
||||
}
|
||||
@ -378,7 +383,7 @@ export function ApiForm({
|
||||
|
||||
form.setFocus(focusField);
|
||||
setInitialFocus(focusField);
|
||||
}, [props.focus, fields, form.setFocus, isLoading, initialFocus]);
|
||||
}, [props.focus, form.setFocus, isLoading, initialFocus]);
|
||||
|
||||
const submitForm: SubmitHandler<FieldValues> = async (data) => {
|
||||
setNonFieldErrors([]);
|
||||
|
@ -16,10 +16,9 @@ import {
|
||||
import { IconCheck } from '@tabler/icons-react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
ImportSessionStatus,
|
||||
useImportSession
|
||||
} from '../../hooks/UseImportSession';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { useImportSession } from '../../hooks/UseImportSession';
|
||||
import useStatusCodes from '../../hooks/UseStatusCodes';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
import ImporterDataSelector from './ImportDataSelector';
|
||||
import ImporterColumnSelector from './ImporterColumnSelector';
|
||||
@ -62,19 +61,23 @@ export default function ImporterDrawer({
|
||||
}) {
|
||||
const session = useImportSession({ sessionId: sessionId });
|
||||
|
||||
const importSessionStatus = useStatusCodes({
|
||||
modelType: ModelType.importsession
|
||||
});
|
||||
|
||||
// Map from import steps to stepper steps
|
||||
const currentStep = useMemo(() => {
|
||||
switch (session.status) {
|
||||
default:
|
||||
case ImportSessionStatus.INITIAL:
|
||||
case importSessionStatus.INITIAL:
|
||||
return 0;
|
||||
case ImportSessionStatus.MAPPING:
|
||||
case importSessionStatus.MAPPING:
|
||||
return 1;
|
||||
case ImportSessionStatus.IMPORTING:
|
||||
case importSessionStatus.IMPORTING:
|
||||
return 2;
|
||||
case ImportSessionStatus.PROCESSING:
|
||||
case importSessionStatus.PROCESSING:
|
||||
return 3;
|
||||
case ImportSessionStatus.COMPLETE:
|
||||
case importSessionStatus.COMPLETE:
|
||||
return 4;
|
||||
}
|
||||
}, [session.status]);
|
||||
@ -85,15 +88,15 @@ export default function ImporterDrawer({
|
||||
}
|
||||
|
||||
switch (session.status) {
|
||||
case ImportSessionStatus.INITIAL:
|
||||
case importSessionStatus.INITIAL:
|
||||
return <Text>Initial : TODO</Text>;
|
||||
case ImportSessionStatus.MAPPING:
|
||||
case importSessionStatus.MAPPING:
|
||||
return <ImporterColumnSelector session={session} />;
|
||||
case ImportSessionStatus.IMPORTING:
|
||||
case importSessionStatus.IMPORTING:
|
||||
return <ImporterImportProgress session={session} />;
|
||||
case ImportSessionStatus.PROCESSING:
|
||||
case importSessionStatus.PROCESSING:
|
||||
return <ImporterDataSelector session={session} />;
|
||||
case ImportSessionStatus.COMPLETE:
|
||||
case importSessionStatus.COMPLETE:
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Alert
|
||||
|
@ -3,10 +3,9 @@ import { Center, Container, Loader, Stack, Text } from '@mantine/core';
|
||||
import { useInterval } from '@mantine/hooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
ImportSessionState,
|
||||
ImportSessionStatus
|
||||
} from '../../hooks/UseImportSession';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { ImportSessionState } from '../../hooks/UseImportSession';
|
||||
import useStatusCodes from '../../hooks/UseStatusCodes';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
|
||||
export default function ImporterImportProgress({
|
||||
@ -14,11 +13,13 @@ export default function ImporterImportProgress({
|
||||
}: {
|
||||
session: ImportSessionState;
|
||||
}) {
|
||||
const importSessionStatus = useStatusCodes({
|
||||
modelType: ModelType.importsession
|
||||
});
|
||||
|
||||
// Periodically refresh the import session data
|
||||
const interval = useInterval(() => {
|
||||
console.log('refreshing:', session.status);
|
||||
|
||||
if (session.status == ImportSessionStatus.IMPORTING) {
|
||||
if (session.status == importSessionStatus.IMPORTING) {
|
||||
session.refreshSession();
|
||||
}
|
||||
}, 1000);
|
||||
|
@ -89,7 +89,11 @@ export function ActionDropdown({
|
||||
{...action.indicator}
|
||||
key={action.name}
|
||||
>
|
||||
<Tooltip label={action.tooltip} hidden={!action.tooltip}>
|
||||
<Tooltip
|
||||
label={action.tooltip}
|
||||
hidden={!action.tooltip}
|
||||
position="left"
|
||||
>
|
||||
<Menu.Item
|
||||
aria-label={id}
|
||||
leftSection={action.icon}
|
||||
@ -229,6 +233,24 @@ export function DeleteItemAction({
|
||||
};
|
||||
}
|
||||
|
||||
export function HoldItemAction({
|
||||
hidden = false,
|
||||
tooltip,
|
||||
onClick
|
||||
}: {
|
||||
hidden?: boolean;
|
||||
tooltip?: string;
|
||||
onClick?: () => void;
|
||||
}): ActionDropdownItem {
|
||||
return {
|
||||
icon: <InvenTreeIcon icon="hold" iconProps={{ color: 'orange' }} />,
|
||||
name: t`Hold`,
|
||||
tooltip: tooltip ?? t`Hold`,
|
||||
onClick: onClick,
|
||||
hidden: hidden
|
||||
};
|
||||
}
|
||||
|
||||
export function CancelItemAction({
|
||||
hidden = false,
|
||||
tooltip,
|
||||
|
@ -10,7 +10,13 @@ export function RenderOwner({
|
||||
instance && (
|
||||
<RenderInlineModel
|
||||
primary={instance.name}
|
||||
suffix={instance.label == 'group' ? <IconUsersGroup /> : <IconUser />}
|
||||
suffix={
|
||||
instance.label == 'group' ? (
|
||||
<IconUsersGroup size={16} />
|
||||
) : (
|
||||
<IconUser size={16} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
@ -62,9 +62,12 @@ export enum ApiEndpoints {
|
||||
|
||||
// Build API endpoints
|
||||
build_order_list = 'build/',
|
||||
build_order_issue = 'build/:id/issue/',
|
||||
build_order_cancel = 'build/:id/cancel/',
|
||||
build_output_create = 'build/:id/create-output/',
|
||||
build_order_hold = 'build/:id/hold/',
|
||||
build_order_complete = 'build/:id/finish/',
|
||||
build_output_complete = 'build/:id/complete/',
|
||||
build_output_create = 'build/:id/create-output/',
|
||||
build_output_scrap = 'build/:id/scrap-outputs/',
|
||||
build_output_delete = 'build/:id/delete-outputs/',
|
||||
build_line_list = 'build/line/',
|
||||
@ -124,14 +127,27 @@ export enum ApiEndpoints {
|
||||
|
||||
// Order API endpoints
|
||||
purchase_order_list = 'order/po/',
|
||||
purchase_order_issue = 'order/po/:id/issue/',
|
||||
purchase_order_hold = 'order/po/:id/hold/',
|
||||
purchase_order_cancel = 'order/po/:id/cancel/',
|
||||
purchase_order_complete = 'order/po/:id/complete/',
|
||||
purchase_order_line_list = 'order/po-line/',
|
||||
purchase_order_receive = 'order/po/:id/receive/',
|
||||
|
||||
sales_order_list = 'order/so/',
|
||||
sales_order_issue = 'order/so/:id/issue/',
|
||||
sales_order_hold = 'order/so/:id/hold/',
|
||||
sales_order_cancel = 'order/so/:id/cancel/',
|
||||
sales_order_ship = 'order/so/:id/ship/',
|
||||
sales_order_complete = 'order/so/:id/complete/',
|
||||
sales_order_line_list = 'order/so-line/',
|
||||
sales_order_shipment_list = 'order/so/shipment/',
|
||||
|
||||
return_order_list = 'order/ro/',
|
||||
return_order_issue = 'order/ro/:id/issue/',
|
||||
return_order_hold = 'order/ro/:id/hold/',
|
||||
return_order_cancel = 'order/ro/:id/cancel/',
|
||||
return_order_complete = 'order/ro/:id/complete/',
|
||||
return_order_line_list = 'order/ro-line/',
|
||||
|
||||
// Template API endpoints
|
||||
|
@ -21,6 +21,7 @@ import { InvenTreeIcon } from '../functions/icons';
|
||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||
import { useBatchCodeGenerator } from '../hooks/UseGenerator';
|
||||
import { apiUrl } from '../states/ApiState';
|
||||
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||
import { PartColumn, StatusColumn } from '../tables/ColumnRenderers';
|
||||
|
||||
/**
|
||||
@ -43,6 +44,8 @@ export function useBuildOrderFields({
|
||||
}
|
||||
});
|
||||
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
reference: {},
|
||||
@ -50,7 +53,13 @@ export function useBuildOrderFields({
|
||||
disabled: !create,
|
||||
filters: {
|
||||
assembly: true,
|
||||
virtual: false
|
||||
virtual: false,
|
||||
active: globalSettings.isSet('BUILDORDER_REQUIRE_ACTIVE_PART')
|
||||
? true
|
||||
: undefined,
|
||||
locked: globalSettings.isSet('BUILDORDER_REQUIRE_LOCKED_PART')
|
||||
? true
|
||||
: undefined
|
||||
},
|
||||
onValueChange(value: any, record?: any) {
|
||||
// Adjust the destination location for the build order
|
||||
@ -107,7 +116,7 @@ export function useBuildOrderFields({
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [create, destination, batchCode]);
|
||||
}, [create, destination, batchCode, globalSettings]);
|
||||
}
|
||||
|
||||
export function useBuildOrderOutputFields({
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
IconBinaryTree2,
|
||||
IconBookmarks,
|
||||
IconBox,
|
||||
IconBrandTelegram,
|
||||
IconBuilding,
|
||||
IconBuildingFactory2,
|
||||
IconBuildingStore,
|
||||
@ -32,6 +33,7 @@ import {
|
||||
IconFlagShare,
|
||||
IconGitBranch,
|
||||
IconGridDots,
|
||||
IconHandStop,
|
||||
IconHash,
|
||||
IconHierarchy,
|
||||
IconInfoCircle,
|
||||
@ -142,6 +144,10 @@ const icons = {
|
||||
plus: IconCirclePlus,
|
||||
minus: IconCircleMinus,
|
||||
cancel: IconCircleX,
|
||||
hold: IconHandStop,
|
||||
issue: IconBrandTelegram,
|
||||
complete: IconCircleCheck,
|
||||
deliver: IconTruckDelivery,
|
||||
|
||||
// Part Icons
|
||||
active: IconCheck,
|
||||
|
@ -1,28 +1,21 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||
import { ModelType } from '../enums/ModelType';
|
||||
import { useInstance } from './UseInstance';
|
||||
import useStatusCodes from './UseStatusCodes';
|
||||
|
||||
/*
|
||||
* Custom hook for managing the state of a data import session
|
||||
*/
|
||||
|
||||
// TODO: Load these values from the server?
|
||||
export enum ImportSessionStatus {
|
||||
INITIAL = 0,
|
||||
MAPPING = 10,
|
||||
IMPORTING = 20,
|
||||
PROCESSING = 30,
|
||||
COMPLETE = 40
|
||||
}
|
||||
|
||||
export type ImportSessionState = {
|
||||
sessionId: number;
|
||||
sessionData: any;
|
||||
setSessionData: (data: any) => void;
|
||||
refreshSession: () => void;
|
||||
sessionQuery: any;
|
||||
status: ImportSessionStatus;
|
||||
status: number;
|
||||
availableFields: Record<string, any>;
|
||||
availableColumns: string[];
|
||||
mappedFields: any[];
|
||||
@ -52,15 +45,17 @@ export function useImportSession({
|
||||
});
|
||||
|
||||
const setSessionData = useCallback((data: any) => {
|
||||
console.log('setting session data:');
|
||||
console.log(data);
|
||||
setInstance(data);
|
||||
}, []);
|
||||
|
||||
const importSessionStatus = useStatusCodes({
|
||||
modelType: ModelType.importsession
|
||||
});
|
||||
|
||||
// Current step of the import process
|
||||
const status: ImportSessionStatus = useMemo(() => {
|
||||
return sessionData?.status ?? ImportSessionStatus.INITIAL;
|
||||
}, [sessionData]);
|
||||
const status: number = useMemo(() => {
|
||||
return sessionData?.status ?? importSessionStatus.INITIAL;
|
||||
}, [sessionData, importSessionStatus]);
|
||||
|
||||
// List of available writeable database field definitions
|
||||
const availableFields: any[] = useMemo(() => {
|
||||
|
46
src/frontend/src/hooks/UseStatusCodes.tsx
Normal file
46
src/frontend/src/hooks/UseStatusCodes.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { getStatusCodes } from '../components/render/StatusRenderer';
|
||||
import { ModelType } from '../enums/ModelType';
|
||||
import { useGlobalStatusState } from '../states/StatusState';
|
||||
|
||||
/**
|
||||
* Hook to access status codes, which are enumerated by the backend.
|
||||
*
|
||||
* This hook is used to return a map of status codes for a given model type.
|
||||
* It is a memoized wrapper around getStatusCodes,
|
||||
* and returns a simplified KEY:value map of status codes.
|
||||
*
|
||||
* e.g. for the "PurchaseOrderStatus" enumeration, returns a map like:
|
||||
*
|
||||
* {
|
||||
* PENDING: 10
|
||||
* PLACED: 20
|
||||
* ON_HOLD: 25,
|
||||
* COMPLETE: 30,
|
||||
* CANCELLED: 40,
|
||||
* LOST: 50,
|
||||
* RETURNED: 60
|
||||
* }
|
||||
*/
|
||||
export default function useStatusCodes({
|
||||
modelType
|
||||
}: {
|
||||
modelType: ModelType | string;
|
||||
}) {
|
||||
const statusCodeList = useGlobalStatusState.getState().status;
|
||||
|
||||
const codes = useMemo(() => {
|
||||
const statusCodes = getStatusCodes(modelType) || {};
|
||||
|
||||
let codesMap: Record<any, any> = {};
|
||||
|
||||
for (let name in statusCodes) {
|
||||
codesMap[name] = statusCodes[name].key;
|
||||
}
|
||||
|
||||
return codesMap;
|
||||
}, [modelType, statusCodeList]);
|
||||
|
||||
return codes;
|
||||
}
|
@ -19,6 +19,7 @@ import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
@ -30,6 +31,7 @@ import {
|
||||
CancelItemAction,
|
||||
DuplicateItemAction,
|
||||
EditItemAction,
|
||||
HoldItemAction,
|
||||
LinkBarcodeAction,
|
||||
UnlinkBarcodeAction,
|
||||
ViewBarcodeAction
|
||||
@ -47,6 +49,7 @@ import {
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import useStatusCodes from '../../hooks/UseStatusCodes';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
||||
@ -364,21 +367,7 @@ export default function BuildDetail() {
|
||||
pk: build.pk,
|
||||
title: t`Edit Build Order`,
|
||||
fields: buildOrderFields,
|
||||
onFormSuccess: () => {
|
||||
refreshInstance();
|
||||
}
|
||||
});
|
||||
|
||||
const cancelBuild = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.build_order_cancel, build.pk),
|
||||
title: t`Cancel Build Order`,
|
||||
fields: {
|
||||
remove_allocated_stock: {},
|
||||
remove_incomplete_outputs: {}
|
||||
},
|
||||
onFormSuccess: () => {
|
||||
refreshInstance();
|
||||
}
|
||||
onFormSuccess: refreshInstance
|
||||
});
|
||||
|
||||
const duplicateBuild = useCreateApiFormModal({
|
||||
@ -393,8 +382,85 @@ export default function BuildDetail() {
|
||||
modelType: ModelType.build
|
||||
});
|
||||
|
||||
const buildStatus = useStatusCodes({ modelType: ModelType.build });
|
||||
|
||||
const cancelOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.build_order_cancel, build.pk),
|
||||
title: t`Cancel Build Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
successMessage: t`Order cancelled`,
|
||||
preFormWarning: t`Cancel this order`,
|
||||
fields: {
|
||||
remove_allocated_stock: {},
|
||||
remove_incomplete_outputs: {}
|
||||
}
|
||||
});
|
||||
|
||||
const holdOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.build_order_hold, build.pk),
|
||||
title: t`Hold Build Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Place this order on hold`,
|
||||
successMessage: t`Order placed on hold`
|
||||
});
|
||||
|
||||
const issueOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.build_order_issue, build.pk),
|
||||
title: t`Issue Build Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Issue this order`,
|
||||
successMessage: t`Order issued`
|
||||
});
|
||||
|
||||
const completeOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.build_order_complete, build.pk),
|
||||
title: t`Complete Build Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Mark this order as complete`,
|
||||
successMessage: t`Order completed`,
|
||||
fields: {
|
||||
accept_overallocated: {},
|
||||
accept_unallocated: {},
|
||||
accept_incomplete: {}
|
||||
}
|
||||
});
|
||||
|
||||
const buildActions = useMemo(() => {
|
||||
const canEdit = user.hasChangeRole(UserRoles.build);
|
||||
|
||||
const canIssue =
|
||||
canEdit &&
|
||||
(build.status == buildStatus.PENDING ||
|
||||
build.status == buildStatus.ON_HOLD);
|
||||
|
||||
const canComplete = canEdit && build.status == buildStatus.PRODUCTION;
|
||||
|
||||
const canHold =
|
||||
canEdit &&
|
||||
(build.status == buildStatus.PENDING ||
|
||||
build.status == buildStatus.PRODUCTION);
|
||||
|
||||
const canCancel =
|
||||
canEdit &&
|
||||
(build.status == buildStatus.PENDING ||
|
||||
build.status == buildStatus.ON_HOLD ||
|
||||
build.status == buildStatus.PRODUCTION);
|
||||
|
||||
return [
|
||||
<PrimaryActionButton
|
||||
title={t`Issue Order`}
|
||||
icon="issue"
|
||||
hidden={!canIssue}
|
||||
color="blue"
|
||||
onClick={issueOrder.open}
|
||||
/>,
|
||||
<PrimaryActionButton
|
||||
title={t`Complete Order`}
|
||||
icon="complete"
|
||||
hidden={!canComplete}
|
||||
color="green"
|
||||
onClick={completeOrder.open}
|
||||
/>,
|
||||
<AdminButton model={ModelType.build} pk={build.pk} />,
|
||||
<BarcodeActionDropdown
|
||||
actions={[
|
||||
@ -421,22 +487,28 @@ export default function BuildDetail() {
|
||||
actions={[
|
||||
EditItemAction({
|
||||
onClick: () => editBuild.open(),
|
||||
hidden: !user.hasChangeRole(UserRoles.build)
|
||||
}),
|
||||
CancelItemAction({
|
||||
tooltip: t`Cancel order`,
|
||||
onClick: () => cancelBuild.open(),
|
||||
hidden: !user.hasChangeRole(UserRoles.build)
|
||||
// TODO: Hide if build cannot be cancelled
|
||||
hidden: !canEdit,
|
||||
tooltip: t`Edit order`
|
||||
}),
|
||||
DuplicateItemAction({
|
||||
onClick: () => duplicateBuild.open(),
|
||||
tooltip: t`Duplicate order`,
|
||||
hidden: !user.hasAddRole(UserRoles.build)
|
||||
}),
|
||||
HoldItemAction({
|
||||
tooltip: t`Hold order`,
|
||||
hidden: !canHold,
|
||||
onClick: holdOrder.open
|
||||
}),
|
||||
CancelItemAction({
|
||||
tooltip: t`Cancel order`,
|
||||
onClick: cancelOrder.open,
|
||||
hidden: !canCancel
|
||||
})
|
||||
]}
|
||||
/>
|
||||
];
|
||||
}, [id, build, user]);
|
||||
}, [id, build, user, buildStatus]);
|
||||
|
||||
const buildBadges = useMemo(() => {
|
||||
return instanceQuery.isFetching
|
||||
@ -454,7 +526,10 @@ export default function BuildDetail() {
|
||||
<>
|
||||
{editBuild.modal}
|
||||
{duplicateBuild.modal}
|
||||
{cancelBuild.modal}
|
||||
{cancelOrder.modal}
|
||||
{holdOrder.modal}
|
||||
{issueOrder.modal}
|
||||
{completeOrder.modal}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<Stack gap="xs">
|
||||
<PageDetail
|
||||
|
@ -19,9 +19,13 @@ import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import NotesEditor from '../../components/editors/NotesEditor';
|
||||
import {
|
||||
ActionDropdown,
|
||||
BarcodeActionDropdown,
|
||||
DeleteItemAction,
|
||||
DuplicateItemAction,
|
||||
EditItemAction
|
||||
EditItemAction,
|
||||
LinkBarcodeAction,
|
||||
UnlinkBarcodeAction,
|
||||
ViewBarcodeAction
|
||||
} from '../../components/items/ActionDropdown';
|
||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
@ -265,6 +269,24 @@ export default function SupplierPartDetail() {
|
||||
const supplierPartActions = useMemo(() => {
|
||||
return [
|
||||
<AdminButton model={ModelType.supplierpart} pk={supplierPart.pk} />,
|
||||
<BarcodeActionDropdown
|
||||
actions={[
|
||||
ViewBarcodeAction({
|
||||
model: ModelType.supplierpart,
|
||||
pk: supplierPart.pk
|
||||
}),
|
||||
LinkBarcodeAction({
|
||||
hidden:
|
||||
supplierPart.barcode_hash ||
|
||||
!user.hasChangeRole(UserRoles.purchase_order)
|
||||
}),
|
||||
UnlinkBarcodeAction({
|
||||
hidden:
|
||||
!supplierPart.barcode_hash ||
|
||||
!user.hasChangeRole(UserRoles.purchase_order)
|
||||
})
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
tooltip={t`Supplier Part Actions`}
|
||||
icon={<IconDots />}
|
||||
|
@ -130,6 +130,13 @@ export default function PartDetail() {
|
||||
icon: 'part',
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'IPN',
|
||||
label: t`IPN`,
|
||||
copy: true,
|
||||
hidden: !part.IPN
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'description',
|
||||
@ -177,13 +184,6 @@ export default function PartDetail() {
|
||||
model: ModelType.stocklocation,
|
||||
hidden: part.default_location || !part.category_default_location
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'IPN',
|
||||
label: t`IPN`,
|
||||
copy: true,
|
||||
hidden: !part.IPN
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'units',
|
||||
@ -799,7 +799,7 @@ export default function PartDetail() {
|
||||
<DetailsBadge
|
||||
label={t`On Order` + `: ${part.ordering}`}
|
||||
color="blue"
|
||||
visible={part.on_order > 0}
|
||||
visible={part.ordering > 0}
|
||||
key="on_order"
|
||||
/>,
|
||||
<DetailsBadge
|
||||
|
@ -12,6 +12,7 @@ import { ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
@ -23,6 +24,7 @@ import {
|
||||
CancelItemAction,
|
||||
DuplicateItemAction,
|
||||
EditItemAction,
|
||||
HoldItemAction,
|
||||
LinkBarcodeAction,
|
||||
UnlinkBarcodeAction,
|
||||
ViewBarcodeAction
|
||||
@ -41,6 +43,8 @@ import {
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import useStatusCodes from '../../hooks/UseStatusCodes';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||
import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable';
|
||||
@ -287,8 +291,77 @@ export default function PurchaseOrderDetail() {
|
||||
];
|
||||
}, [order, id, user]);
|
||||
|
||||
const poStatus = useStatusCodes({ modelType: ModelType.purchaseorder });
|
||||
|
||||
const issueOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.purchase_order_issue, order.pk),
|
||||
title: t`Issue Purchase Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Issue this order`,
|
||||
successMessage: t`Order issued`
|
||||
});
|
||||
|
||||
const cancelOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.purchase_order_cancel, order.pk),
|
||||
title: t`Cancel Purchase Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Cancel this order`,
|
||||
successMessage: t`Order cancelled`
|
||||
});
|
||||
|
||||
const holdOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.purchase_order_hold, order.pk),
|
||||
title: t`Hold Purchase Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Place this order on hold`,
|
||||
successMessage: t`Order placed on hold`
|
||||
});
|
||||
|
||||
const completeOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.purchase_order_complete, order.pk),
|
||||
title: t`Complete Purchase Order`,
|
||||
successMessage: t`Order completed`,
|
||||
timeout: 10000,
|
||||
fields: {
|
||||
accept_incomplete: {}
|
||||
},
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Mark this order as complete`
|
||||
});
|
||||
|
||||
const poActions = useMemo(() => {
|
||||
const canEdit: boolean = user.hasChangeRole(UserRoles.purchase_order);
|
||||
|
||||
const canIssue: boolean =
|
||||
canEdit &&
|
||||
(order.status == poStatus.PENDING || order.status == poStatus.ON_HOLD);
|
||||
|
||||
const canHold: boolean =
|
||||
canEdit &&
|
||||
(order.status == poStatus.PENDING || order.status == poStatus.PLACED);
|
||||
|
||||
const canComplete: boolean = canEdit && order.status == poStatus.PLACED;
|
||||
|
||||
const canCancel: boolean =
|
||||
canEdit &&
|
||||
order.status != poStatus.CANCELLED &&
|
||||
order.status != poStatus.COMPLETE;
|
||||
|
||||
return [
|
||||
<PrimaryActionButton
|
||||
title={t`Issue Order`}
|
||||
icon="issue"
|
||||
hidden={!canIssue}
|
||||
color="blue"
|
||||
onClick={issueOrder.open}
|
||||
/>,
|
||||
<PrimaryActionButton
|
||||
title={t`Complete Order`}
|
||||
icon="complete"
|
||||
hidden={!canComplete}
|
||||
color="green"
|
||||
onClick={completeOrder.open}
|
||||
/>,
|
||||
<AdminButton model={ModelType.purchaseorder} pk={order.pk} />,
|
||||
<BarcodeActionDropdown
|
||||
actions={[
|
||||
@ -314,22 +387,31 @@ export default function PurchaseOrderDetail() {
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
EditItemAction({
|
||||
hidden: !user.hasChangeRole(UserRoles.purchase_order),
|
||||
hidden: !canEdit,
|
||||
tooltip: t`Edit order`,
|
||||
onClick: () => {
|
||||
editPurchaseOrder.open();
|
||||
}
|
||||
}),
|
||||
CancelItemAction({
|
||||
tooltip: t`Cancel order`
|
||||
}),
|
||||
DuplicateItemAction({
|
||||
hidden: !user.hasAddRole(UserRoles.purchase_order),
|
||||
onClick: () => duplicatePurchaseOrder.open()
|
||||
onClick: () => duplicatePurchaseOrder.open(),
|
||||
tooltip: t`Duplicate order`
|
||||
}),
|
||||
HoldItemAction({
|
||||
tooltip: t`Hold order`,
|
||||
hidden: !canHold,
|
||||
onClick: holdOrder.open
|
||||
}),
|
||||
CancelItemAction({
|
||||
tooltip: t`Cancel order`,
|
||||
hidden: !canCancel,
|
||||
onClick: cancelOrder.open
|
||||
})
|
||||
]}
|
||||
/>
|
||||
];
|
||||
}, [id, order, user]);
|
||||
}, [id, order, user, poStatus]);
|
||||
|
||||
const orderBadges: ReactNode[] = useMemo(() => {
|
||||
return instanceQuery.isLoading
|
||||
@ -345,7 +427,12 @@ export default function PurchaseOrderDetail() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueOrder.modal}
|
||||
{holdOrder.modal}
|
||||
{cancelOrder.modal}
|
||||
{completeOrder.modal}
|
||||
{editPurchaseOrder.modal}
|
||||
{duplicatePurchaseOrder.modal}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<Stack gap="xs">
|
||||
<PageDetail
|
||||
|
@ -11,6 +11,7 @@ import { ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
@ -22,6 +23,7 @@ import {
|
||||
CancelItemAction,
|
||||
DuplicateItemAction,
|
||||
EditItemAction,
|
||||
HoldItemAction,
|
||||
LinkBarcodeAction,
|
||||
UnlinkBarcodeAction,
|
||||
ViewBarcodeAction
|
||||
@ -40,6 +42,8 @@ import {
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import useStatusCodes from '../../hooks/UseStatusCodes';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||
import ReturnOrderLineItemTable from '../../tables/sales/ReturnOrderLineItemTable';
|
||||
@ -101,7 +105,7 @@ export default function ReturnOrderDetail() {
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.salesorder
|
||||
model: ModelType.returnorder
|
||||
}
|
||||
];
|
||||
|
||||
@ -120,15 +124,6 @@ export default function ReturnOrderDetail() {
|
||||
total: order.line_items,
|
||||
progress: order.completed_lines
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'shipments',
|
||||
icon: 'shipment',
|
||||
label: t`Completed Shipments`,
|
||||
total: order.shipments,
|
||||
progress: order.completed_shipments
|
||||
// TODO: Fix this progress bar
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'currency',
|
||||
@ -296,8 +291,77 @@ export default function ReturnOrderDetail() {
|
||||
follow: true
|
||||
});
|
||||
|
||||
const issueOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.return_order_issue, order.pk),
|
||||
title: t`Issue Return Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Issue this order`,
|
||||
successMessage: t`Order issued`
|
||||
});
|
||||
|
||||
const cancelOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.return_order_cancel, order.pk),
|
||||
title: t`Cancel Return Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Cancel this order`,
|
||||
successMessage: t`Order canceled`
|
||||
});
|
||||
|
||||
const holdOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.return_order_hold, order.pk),
|
||||
title: t`Hold Return Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Place this order on hold`,
|
||||
successMessage: t`Order placed on hold`
|
||||
});
|
||||
|
||||
const completeOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.return_order_complete, order.pk),
|
||||
title: t`Complete Return Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Mark this order as complete`,
|
||||
successMessage: t`Order completed`
|
||||
});
|
||||
|
||||
const roStatus = useStatusCodes({ modelType: ModelType.returnorder });
|
||||
|
||||
const orderActions = useMemo(() => {
|
||||
const canEdit: boolean = user.hasChangeRole(UserRoles.return_order);
|
||||
|
||||
const canIssue: boolean =
|
||||
canEdit &&
|
||||
(order.status == roStatus.PENDING || order.status == roStatus.ON_HOLD);
|
||||
|
||||
const canHold: boolean =
|
||||
canEdit &&
|
||||
(order.status == roStatus.PENDING ||
|
||||
order.status == roStatus.PLACED ||
|
||||
order.status == roStatus.IN_PROGRESS);
|
||||
|
||||
const canCancel: boolean =
|
||||
canEdit &&
|
||||
(order.status == roStatus.PENDING ||
|
||||
order.status == roStatus.IN_PROGRESS ||
|
||||
order.status == roStatus.ON_HOLD);
|
||||
|
||||
const canComplete: boolean =
|
||||
canEdit && order.status == roStatus.IN_PROGRESS;
|
||||
|
||||
return [
|
||||
<PrimaryActionButton
|
||||
title={t`Issue Order`}
|
||||
icon="issue"
|
||||
hidden={!canIssue}
|
||||
color="blue"
|
||||
onClick={() => issueOrder.open()}
|
||||
/>,
|
||||
<PrimaryActionButton
|
||||
title={t`Complete Order`}
|
||||
icon="complete"
|
||||
hidden={!canComplete}
|
||||
color="green"
|
||||
onClick={() => completeOrder.open()}
|
||||
/>,
|
||||
<AdminButton model={ModelType.returnorder} pk={order.pk} />,
|
||||
<BarcodeActionDropdown
|
||||
actions={[
|
||||
@ -324,25 +388,38 @@ export default function ReturnOrderDetail() {
|
||||
actions={[
|
||||
EditItemAction({
|
||||
hidden: !user.hasChangeRole(UserRoles.return_order),
|
||||
tooltip: t`Edit order`,
|
||||
onClick: () => {
|
||||
editReturnOrder.open();
|
||||
}
|
||||
}),
|
||||
CancelItemAction({
|
||||
tooltip: t`Cancel order`
|
||||
}),
|
||||
DuplicateItemAction({
|
||||
tooltip: t`Duplicate order`,
|
||||
hidden: !user.hasChangeRole(UserRoles.return_order),
|
||||
onClick: () => duplicateReturnOrder.open()
|
||||
}),
|
||||
HoldItemAction({
|
||||
tooltip: t`Hold order`,
|
||||
hidden: !canHold,
|
||||
onClick: () => holdOrder.open()
|
||||
}),
|
||||
CancelItemAction({
|
||||
tooltip: t`Cancel order`,
|
||||
hidden: !canCancel,
|
||||
onClick: () => cancelOrder.open()
|
||||
})
|
||||
]}
|
||||
/>
|
||||
];
|
||||
}, [user, order]);
|
||||
}, [user, order, roStatus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{editReturnOrder.modal}
|
||||
{issueOrder.modal}
|
||||
{cancelOrder.modal}
|
||||
{holdOrder.modal}
|
||||
{completeOrder.modal}
|
||||
{duplicateReturnOrder.modal}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<Stack gap="xs">
|
||||
|
@ -13,6 +13,7 @@ import { ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
@ -24,6 +25,7 @@ import {
|
||||
CancelItemAction,
|
||||
DuplicateItemAction,
|
||||
EditItemAction,
|
||||
HoldItemAction,
|
||||
LinkBarcodeAction,
|
||||
UnlinkBarcodeAction,
|
||||
ViewBarcodeAction
|
||||
@ -42,6 +44,8 @@ import {
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import useStatusCodes from '../../hooks/UseStatusCodes';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||
@ -213,6 +217,8 @@ export default function SalesOrderDetail() {
|
||||
);
|
||||
}, [order, instanceQuery]);
|
||||
|
||||
const soStatus = useStatusCodes({ modelType: ModelType.salesorder });
|
||||
|
||||
const salesOrderFields = useSalesOrderFields();
|
||||
|
||||
const editSalesOrder = useEditApiFormModal({
|
||||
@ -253,6 +259,10 @@ export default function SalesOrderDetail() {
|
||||
<SalesOrderLineItemTable
|
||||
orderId={order.pk}
|
||||
customerId={order.customer}
|
||||
editable={
|
||||
order.status != soStatus.COMPLETE &&
|
||||
order.status != soStatus.CANCELLED
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@ -296,10 +306,86 @@ export default function SalesOrderDetail() {
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [order, id, user]);
|
||||
}, [order, id, user, soStatus]);
|
||||
|
||||
const issueOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.sales_order_issue, order.pk),
|
||||
title: t`Issue Sales Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Issue this order`,
|
||||
successMessage: t`Order issued`
|
||||
});
|
||||
|
||||
const cancelOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.sales_order_cancel, order.pk),
|
||||
title: t`Cancel Sales Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Cancel this order`,
|
||||
successMessage: t`Order cancelled`
|
||||
});
|
||||
|
||||
const holdOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.sales_order_hold, order.pk),
|
||||
title: t`Hold Sales Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Place this order on hold`,
|
||||
successMessage: t`Order placed on hold`
|
||||
});
|
||||
|
||||
const completeOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.sales_order_complete, order.pk),
|
||||
title: t`Complete Sales Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Mark this order as complete`,
|
||||
successMessage: t`Order completed`,
|
||||
fields: {
|
||||
accept_incomplete: {}
|
||||
}
|
||||
});
|
||||
|
||||
const soActions = useMemo(() => {
|
||||
const canEdit: boolean = user.hasChangeRole(UserRoles.sales_order);
|
||||
|
||||
const canIssue: boolean =
|
||||
canEdit &&
|
||||
(order.status == soStatus.PENDING || order.status == soStatus.ON_HOLD);
|
||||
|
||||
const canCancel: boolean =
|
||||
canEdit &&
|
||||
(order.status == soStatus.PENDING ||
|
||||
order.status == soStatus.ON_HOLD ||
|
||||
order.status == soStatus.IN_PROGRESS);
|
||||
|
||||
const canHold: boolean =
|
||||
canEdit &&
|
||||
(order.status == soStatus.PENDING ||
|
||||
order.status == soStatus.IN_PROGRESS);
|
||||
|
||||
const canShip: boolean = canEdit && order.status == soStatus.IN_PROGRESS;
|
||||
const canComplete: boolean = canEdit && order.status == soStatus.SHIPPED;
|
||||
|
||||
return [
|
||||
<PrimaryActionButton
|
||||
title={t`Issue Order`}
|
||||
icon="issue"
|
||||
hidden={!canIssue}
|
||||
color="blue"
|
||||
onClick={issueOrder.open}
|
||||
/>,
|
||||
<PrimaryActionButton
|
||||
title={t`Ship Order`}
|
||||
icon="deliver"
|
||||
hidden={!canShip}
|
||||
color="blue"
|
||||
onClick={completeOrder.open}
|
||||
/>,
|
||||
<PrimaryActionButton
|
||||
title={t`Complete Order`}
|
||||
icon="complete"
|
||||
hidden={!canComplete}
|
||||
color="green"
|
||||
onClick={completeOrder.open}
|
||||
/>,
|
||||
<AdminButton model={ModelType.salesorder} pk={order.pk} />,
|
||||
<BarcodeActionDropdown
|
||||
actions={[
|
||||
@ -325,20 +411,29 @@ export default function SalesOrderDetail() {
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
EditItemAction({
|
||||
hidden: !user.hasChangeRole(UserRoles.sales_order),
|
||||
onClick: () => editSalesOrder.open()
|
||||
}),
|
||||
CancelItemAction({
|
||||
tooltip: t`Cancel order`
|
||||
hidden: !canEdit,
|
||||
onClick: () => editSalesOrder.open(),
|
||||
tooltip: t`Edit order`
|
||||
}),
|
||||
DuplicateItemAction({
|
||||
hidden: !user.hasAddRole(UserRoles.sales_order),
|
||||
onClick: () => duplicateSalesOrder.open()
|
||||
onClick: () => duplicateSalesOrder.open(),
|
||||
tooltip: t`Duplicate order`
|
||||
}),
|
||||
HoldItemAction({
|
||||
tooltip: t`Hold order`,
|
||||
hidden: !canHold,
|
||||
onClick: () => holdOrder.open()
|
||||
}),
|
||||
CancelItemAction({
|
||||
tooltip: t`Cancel order`,
|
||||
hidden: !canCancel,
|
||||
onClick: () => cancelOrder.open()
|
||||
})
|
||||
]}
|
||||
/>
|
||||
];
|
||||
}, [user, order]);
|
||||
}, [user, order, soStatus]);
|
||||
|
||||
const orderBadges: ReactNode[] = useMemo(() => {
|
||||
return instanceQuery.isLoading
|
||||
@ -355,7 +450,12 @@ export default function SalesOrderDetail() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueOrder.modal}
|
||||
{cancelOrder.modal}
|
||||
{holdOrder.modal}
|
||||
{completeOrder.modal}
|
||||
{editSalesOrder.modal}
|
||||
{duplicateSalesOrder.modal}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<Stack gap="xs">
|
||||
<PageDetail
|
||||
|
@ -112,6 +112,7 @@ export function RowActions({
|
||||
withinPortal={true}
|
||||
label={action.tooltip ?? action.title}
|
||||
key={action.title}
|
||||
position="left"
|
||||
>
|
||||
<Menu.Item
|
||||
color={action.color}
|
||||
|
@ -33,6 +33,11 @@ export function UsedInTable({
|
||||
title: t`Assembly`,
|
||||
render: (record: any) => PartColumn(record.part_detail)
|
||||
},
|
||||
{
|
||||
accessor: 'part_detail.IPN',
|
||||
sortable: false,
|
||||
title: t`IPN`
|
||||
},
|
||||
{
|
||||
accessor: 'sub_part',
|
||||
sortable: true,
|
||||
|
@ -34,10 +34,12 @@ import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
export default function SalesOrderLineItemTable({
|
||||
orderId,
|
||||
customerId
|
||||
customerId,
|
||||
editable
|
||||
}: {
|
||||
orderId: number;
|
||||
customerId: number;
|
||||
editable: boolean;
|
||||
}) {
|
||||
const user = useUserState();
|
||||
const table = useTable('sales-order-line-item');
|
||||
@ -207,7 +209,7 @@ export default function SalesOrderLineItemTable({
|
||||
});
|
||||
newLine.open();
|
||||
}}
|
||||
hidden={!user.hasAddRole(UserRoles.sales_order)}
|
||||
hidden={!editable || !user.hasAddRole(UserRoles.sales_order)}
|
||||
/>
|
||||
];
|
||||
}, [user, orderId]);
|
||||
@ -218,7 +220,10 @@ export default function SalesOrderLineItemTable({
|
||||
|
||||
return [
|
||||
{
|
||||
hidden: allocated || !user.hasChangeRole(UserRoles.sales_order),
|
||||
hidden:
|
||||
allocated ||
|
||||
!editable ||
|
||||
!user.hasChangeRole(UserRoles.sales_order),
|
||||
title: t`Allocate stock`,
|
||||
icon: <IconSquareArrowRight />,
|
||||
color: 'green'
|
||||
@ -242,21 +247,21 @@ export default function SalesOrderLineItemTable({
|
||||
color: 'blue'
|
||||
},
|
||||
RowEditAction({
|
||||
hidden: !user.hasChangeRole(UserRoles.sales_order),
|
||||
hidden: !editable || !user.hasChangeRole(UserRoles.sales_order),
|
||||
onClick: () => {
|
||||
setSelectedLine(record.pk);
|
||||
editLine.open();
|
||||
}
|
||||
}),
|
||||
RowDuplicateAction({
|
||||
hidden: !user.hasAddRole(UserRoles.sales_order),
|
||||
hidden: !editable || !user.hasAddRole(UserRoles.sales_order),
|
||||
onClick: () => {
|
||||
setInitialData(record);
|
||||
newLine.open();
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
hidden: !user.hasDeleteRole(UserRoles.sales_order),
|
||||
hidden: !editable || !user.hasDeleteRole(UserRoles.sales_order),
|
||||
onClick: () => {
|
||||
setSelectedLine(record.pk);
|
||||
deleteLine.open();
|
||||
@ -264,7 +269,7 @@ export default function SalesOrderLineItemTable({
|
||||
})
|
||||
];
|
||||
},
|
||||
[user]
|
||||
[user, editable]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
LineItemsProgressColumn,
|
||||
ProjectCodeColumn,
|
||||
ReferenceColumn,
|
||||
ResponsibleColumn,
|
||||
ShipmentDateColumn,
|
||||
StatusColumn,
|
||||
TargetDateColumn
|
||||
@ -129,6 +130,7 @@ export function SalesOrderTable({
|
||||
CreationDateColumn({}),
|
||||
TargetDateColumn({}),
|
||||
ShipmentDateColumn({}),
|
||||
ResponsibleColumn({}),
|
||||
{
|
||||
accessor: 'total_price',
|
||||
title: t`Total Price`,
|
||||
|
@ -59,6 +59,7 @@ export const test = baseTest.extend({
|
||||
if (
|
||||
msg.type() === 'error' &&
|
||||
!msg.text().startsWith('ERR: ') &&
|
||||
msg.text().indexOf('downloadable font: download failed') < 0 &&
|
||||
msg
|
||||
.text()
|
||||
.indexOf(
|
||||
|
@ -9,8 +9,40 @@ test('PUI - Pages - Build Order', async ({ page }) => {
|
||||
|
||||
// Navigate to the correct build order
|
||||
await page.getByRole('tab', { name: 'Build', exact: true }).click();
|
||||
|
||||
// We have now loaded the "Build Order" table. Check for some expected texts
|
||||
await page.getByText('On Hold').waitFor();
|
||||
await page.getByText('Pending').first().waitFor();
|
||||
|
||||
// Load a particular build order
|
||||
await page.getByRole('cell', { name: 'BO0017' }).click();
|
||||
|
||||
// This build order should be "on hold"
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
await page.getByRole('button', { name: 'Issue Order' }).click();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
// Back to the build list
|
||||
await page.getByLabel('breadcrumb-0-build-orders').click();
|
||||
|
||||
// Load a different build order
|
||||
await page.getByRole('cell', { name: 'BO0011' }).click();
|
||||
|
||||
// This build order should be "in production"
|
||||
await page.getByText('Production').first().waitFor();
|
||||
await page.getByRole('button', { name: 'Complete Order' }).click();
|
||||
await page.getByText('Accept Unallocated').waitFor();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
// Check for other expected actions
|
||||
await page.getByLabel('action-menu-build-order-').click();
|
||||
await page.getByLabel('action-menu-build-order-actions-edit').waitFor();
|
||||
await page.getByLabel('action-menu-build-order-actions-duplicate').waitFor();
|
||||
await page.getByLabel('action-menu-build-order-actions-hold').waitFor();
|
||||
await page.getByLabel('action-menu-build-order-actions-cancel').click();
|
||||
await page.getByText('Remove Incomplete Outputs').waitFor();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
// Click on some tabs
|
||||
await page.getByRole('tab', { name: 'Attachments' }).click();
|
||||
await page.getByRole('tab', { name: 'Notes' }).click();
|
||||
|
67
src/frontend/tests/pages/pui_orders.spec.ts
Normal file
67
src/frontend/tests/pages/pui_orders.spec.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { test } from '../baseFixtures.ts';
|
||||
import { baseUrl } from '../defaults.ts';
|
||||
import { doQuickLogin } from '../login.ts';
|
||||
|
||||
test('PUI - Sales Orders', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.goto(`${baseUrl}/home`);
|
||||
await page.getByRole('tab', { name: 'Sales' }).click();
|
||||
await page.getByRole('tab', { name: 'Sales Orders' }).click();
|
||||
|
||||
// Check for expected text in the table
|
||||
await page.getByRole('tab', { name: 'Sales Orders' }).waitFor();
|
||||
await page.getByText('In Progress').first().waitFor();
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
|
||||
// Navigate to a particular sales order
|
||||
await page.getByRole('cell', { name: 'SO0003' }).click();
|
||||
|
||||
// Order is "on hold". We will "issue" it and then place on hold again
|
||||
await page.getByText('Sales Order: SO0003').waitFor();
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
await page.getByRole('button', { name: 'Issue Order' }).click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Order should now be "in progress"
|
||||
await page.getByText('In Progress').first().waitFor();
|
||||
await page.getByRole('button', { name: 'Ship Order' }).waitFor();
|
||||
|
||||
await page.getByLabel('action-menu-order-actions').click();
|
||||
|
||||
await page.getByLabel('action-menu-order-actions-edit').waitFor();
|
||||
await page.getByLabel('action-menu-order-actions-duplicate').waitFor();
|
||||
await page.getByLabel('action-menu-order-actions-cancel').waitFor();
|
||||
|
||||
// Mark the order as "on hold" again
|
||||
await page.getByLabel('action-menu-order-actions-hold').click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
|
||||
});
|
||||
|
||||
test('PUI - Purchase Orders', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.goto(`${baseUrl}/home`);
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await page.getByRole('tab', { name: 'Purchase Orders' }).click();
|
||||
|
||||
// Check for expected values
|
||||
await page.getByRole('cell', { name: 'PO0014' }).waitFor();
|
||||
await page.getByText('Wire-E-Coyote').waitFor();
|
||||
await page.getByText('Cancelled').first().waitFor();
|
||||
await page.getByText('Pending').first().waitFor();
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
|
||||
// Click through to a particular purchase order
|
||||
await page.getByRole('cell', { name: 'PO0013' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
|
||||
|
||||
// Display QR code
|
||||
await page.getByLabel('action-menu-barcode-actions').click();
|
||||
await page.getByLabel('action-menu-barcode-actions-view').click();
|
||||
await page.getByRole('img', { name: 'QR Code' }).waitFor();
|
||||
});
|
Reference in New Issue
Block a user