diff --git a/src/frontend/src/components/items/Expand.tsx b/src/frontend/src/components/items/Expand.tsx
new file mode 100644
index 0000000000..ae12cacd06
--- /dev/null
+++ b/src/frontend/src/components/items/Expand.tsx
@@ -0,0 +1,14 @@
+import type { ReactNode } from 'react';
+
+/**
+ * A component that expands to fill the available space
+ */
+export default function Expand({
+  children,
+  flex
+}: {
+  children: ReactNode;
+  flex?: number;
+}) {
+  return <div style={{ flexGrow: flex ?? 1 }}>{children}</div>;
+}
diff --git a/src/frontend/src/components/render/Company.tsx b/src/frontend/src/components/render/Company.tsx
index d0d1c6c47a..75c713e4c9 100644
--- a/src/frontend/src/components/render/Company.tsx
+++ b/src/frontend/src/components/render/Company.tsx
@@ -70,7 +70,9 @@ export function RenderSupplierPart(
       {...props}
       primary={supplier?.name}
       secondary={instance.SKU}
-      image={part?.thumbnail ?? part?.image}
+      image={
+        part?.thumbnail ?? part?.image ?? supplier?.thumbnail ?? supplier?.image
+      }
       suffix={
         part.full_name ? <Text size='sm'>{part.full_name}</Text> : undefined
       }
diff --git a/src/frontend/src/components/wizards/OrderPartsWizard.tsx b/src/frontend/src/components/wizards/OrderPartsWizard.tsx
new file mode 100644
index 0000000000..1076eb6df8
--- /dev/null
+++ b/src/frontend/src/components/wizards/OrderPartsWizard.tsx
@@ -0,0 +1,420 @@
+import { t } from '@lingui/macro';
+import { Alert, Group, Paper, Tooltip } from '@mantine/core';
+import { showNotification } from '@mantine/notifications';
+import { IconShoppingCart } from '@tabler/icons-react';
+import { DataTable } from 'mantine-datatable';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { ApiEndpoints } from '../../enums/ApiEndpoints';
+import { ModelType } from '../../enums/ModelType';
+import { useSupplierPartFields } from '../../forms/CompanyForms';
+import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
+import { useCreateApiFormModal } from '../../hooks/UseForm';
+import useWizard from '../../hooks/UseWizard';
+import { apiUrl } from '../../states/ApiState';
+import { PartColumn } from '../../tables/ColumnRenderers';
+import { ActionButton } from '../buttons/ActionButton';
+import { AddItemButton } from '../buttons/AddItemButton';
+import RemoveRowButton from '../buttons/RemoveRowButton';
+import { StandaloneField } from '../forms/StandaloneField';
+import type { ApiFormFieldSet } from '../forms/fields/ApiFormField';
+import Expand from '../items/Expand';
+
+/**
+ * Attributes for each selected part
+ * - part: The part instance
+ * - supplier_part: The selected supplier part instance
+ * - purchase_order: The selected purchase order instance
+ * - quantity: The quantity of the part to order
+ * - errors: Error messages for each attribute
+ */
+interface PartOrderRecord {
+  part: any;
+  supplier_part: any;
+  purchase_order: any;
+  quantity: number;
+  errors: any;
+}
+
+function SelectPartsStep({
+  records,
+  onRemovePart,
+  onSelectSupplierPart,
+  onSelectPurchaseOrder
+}: {
+  records: PartOrderRecord[];
+  onRemovePart: (part: any) => void;
+  onSelectSupplierPart: (partId: number, supplierPart: any) => void;
+  onSelectPurchaseOrder: (partId: number, purchaseOrder: any) => void;
+}) {
+  const [selectedRecord, setSelectedRecord] = useState<PartOrderRecord | null>(
+    null
+  );
+
+  const purchaseOrderFields = usePurchaseOrderFields({
+    supplierId: selectedRecord?.supplier_part?.supplier
+  });
+
+  const newPurchaseOrder = useCreateApiFormModal({
+    url: apiUrl(ApiEndpoints.purchase_order_list),
+    title: t`New Purchase Order`,
+    fields: purchaseOrderFields,
+    successMessage: t`Purchase order created`,
+    onFormSuccess: (response: any) => {
+      onSelectPurchaseOrder(selectedRecord?.part.pk, response);
+    }
+  });
+
+  const supplierPartFields = useSupplierPartFields({
+    partId: selectedRecord?.part.pk
+  });
+
+  const newSupplierPart = useCreateApiFormModal({
+    url: apiUrl(ApiEndpoints.supplier_part_list),
+    title: t`New Supplier Part`,
+    fields: supplierPartFields,
+    successMessage: t`Supplier part created`,
+    onFormSuccess: (response: any) => {
+      onSelectSupplierPart(selectedRecord?.part.pk, response);
+    }
+  });
+
+  const addToOrderFields: ApiFormFieldSet = useMemo(() => {
+    return {
+      order: {
+        value: selectedRecord?.purchase_order?.pk,
+        disabled: true
+      },
+      part: {
+        value: selectedRecord?.supplier_part?.pk,
+        disabled: true
+      },
+      reference: {},
+      quantity: {
+        // TODO: Auto-fill with the desired quantity
+      },
+      merge_items: {}
+    };
+  }, [selectedRecord]);
+
+  const addToOrder = useCreateApiFormModal({
+    url: apiUrl(ApiEndpoints.purchase_order_line_list),
+    title: t`Add to Purchase Order`,
+    fields: addToOrderFields,
+    focus: 'quantity',
+    initialData: {
+      order: selectedRecord?.purchase_order?.pk,
+      part: selectedRecord?.supplier_part?.pk,
+      quantity: selectedRecord?.quantity
+    },
+    onFormSuccess: (response: any) => {
+      // Remove the row from the list
+      onRemovePart(selectedRecord?.part);
+    },
+    successMessage: t`Part added to purchase order`
+  });
+
+  const columns: any[] = useMemo(() => {
+    return [
+      {
+        accessor: 'left_actions',
+        title: ' ',
+        width: '1%',
+        render: (record: PartOrderRecord) => (
+          <Group gap='xs' wrap='nowrap' justify='left'>
+            <RemoveRowButton onClick={() => onRemovePart(record.part)} />
+          </Group>
+        )
+      },
+      {
+        accessor: 'part',
+        title: t`Part`,
+        render: (record: PartOrderRecord) => (
+          <Tooltip label={record.part?.description}>
+            <Paper p='xs'>
+              <PartColumn part={record.part} />
+            </Paper>
+          </Tooltip>
+        )
+      },
+      {
+        accessor: 'supplier_part',
+        title: t`Supplier Part`,
+        width: '40%',
+        render: (record: PartOrderRecord) => (
+          <Group gap='xs' wrap='nowrap' justify='left'>
+            <Expand>
+              <StandaloneField
+                fieldName='supplier_part'
+                hideLabels={true}
+                error={record.errors?.supplier_part}
+                fieldDefinition={{
+                  field_type: 'related field',
+                  api_url: apiUrl(ApiEndpoints.supplier_part_list),
+                  model: ModelType.supplierpart,
+                  required: true,
+                  value: record.supplier_part?.pk,
+                  onValueChange: (value, instance) => {
+                    onSelectSupplierPart(record.part.pk, instance);
+                  },
+                  filters: {
+                    part: record.part.pk,
+                    active: true,
+                    supplier_detail: true
+                  }
+                }}
+              />
+            </Expand>
+            <AddItemButton
+              tooltip={t`New supplier part`}
+              tooltipAlignment='top'
+              onClick={() => {
+                setSelectedRecord(record);
+                newSupplierPart.open();
+              }}
+            />
+          </Group>
+        )
+      },
+      {
+        accessor: 'purchase_order',
+        title: t`Purchase Order`,
+        width: '40%',
+        render: (record: PartOrderRecord) => (
+          <Group gap='xs' wrap='nowrap' justify='left'>
+            <Expand>
+              <StandaloneField
+                fieldName='purchase_order'
+                hideLabels={true}
+                fieldDefinition={{
+                  field_type: 'related field',
+                  api_url: apiUrl(ApiEndpoints.purchase_order_list),
+                  model: ModelType.purchaseorder,
+                  disabled: !record.supplier_part?.supplier,
+                  value: record.purchase_order?.pk,
+                  filters: {
+                    supplier: record.supplier_part?.supplier,
+                    outstanding: true
+                  },
+                  onValueChange: (value, instance) => {
+                    onSelectPurchaseOrder(record.part.pk, instance);
+                  }
+                }}
+              />
+            </Expand>
+            <AddItemButton
+              tooltip={t`New purchase order`}
+              tooltipAlignment='top'
+              disabled={!record.supplier_part?.pk}
+              onClick={() => {
+                setSelectedRecord(record);
+                newPurchaseOrder.open();
+              }}
+            />
+          </Group>
+        )
+      },
+      {
+        accessor: 'right_actions',
+        title: ' ',
+        width: '1%',
+        render: (record: PartOrderRecord) => (
+          <Group grow gap='xs' wrap='nowrap' justify='right'>
+            <ActionButton
+              onClick={() => {
+                setSelectedRecord(record);
+                addToOrder.open();
+              }}
+              disabled={
+                !record.supplier_part?.pk ||
+                !record.quantity ||
+                !record.purchase_order?.pk
+              }
+              icon={<IconShoppingCart />}
+              tooltip={t`Add to selected purchase order`}
+              tooltipAlignment='top'
+              color='blue'
+            />
+          </Group>
+        )
+      }
+    ];
+  }, [onRemovePart]);
+
+  if (records.length === 0) {
+    return (
+      <Alert color='red' title={t`No parts selected`}>
+        {t`No purchaseable parts selected`}
+      </Alert>
+    );
+  }
+
+  return (
+    <>
+      <DataTable idAccessor='part.pk' columns={columns} records={records} />
+      {newPurchaseOrder.modal}
+      {newSupplierPart.modal}
+      {addToOrder.modal}
+    </>
+  );
+}
+
+export default function OrderPartsWizard({
+  parts
+}: {
+  parts: any[];
+}) {
+  // Track a list of selected parts
+  const [selectedParts, setSelectedParts] = useState<PartOrderRecord[]>([]);
+
+  // Remove a part from the selected parts list
+  const removePart = useCallback(
+    (part: any) => {
+      const records = selectedParts.filter(
+        (record: PartOrderRecord) => record.part?.pk !== part.pk
+      );
+
+      setSelectedParts(records);
+
+      // If no parts remain, close the wizard
+      if (records.length === 0) {
+        wizard.closeWizard();
+        showNotification({
+          title: t`Parts Added`,
+          message: t`All selected parts added to a purchase order`,
+          color: 'green'
+        });
+      }
+    },
+    [selectedParts]
+  );
+
+  // Select a supplier part for a part
+  const selectSupplierPart = useCallback(
+    (partId: number, supplierPart: any) => {
+      const records = [...selectedParts];
+
+      records.forEach((record: PartOrderRecord, index: number) => {
+        if (record.part.pk === partId) {
+          records[index].supplier_part = supplierPart;
+        }
+      });
+
+      setSelectedParts(records);
+    },
+    [selectedParts]
+  );
+
+  // Select purchase order for a part
+  const selectPurchaseOrder = useCallback(
+    (partId: number, purchaseOrder: any) => {
+      const records = [...selectedParts];
+
+      records.forEach((record: PartOrderRecord, index: number) => {
+        if (record.part.pk === partId) {
+          records[index].purchase_order = purchaseOrder;
+        }
+      });
+
+      setSelectedParts(records);
+    },
+    [selectedParts]
+  );
+
+  // Render the select wizard step
+  const renderStep = useCallback(
+    (step: number) => {
+      return (
+        <SelectPartsStep
+          records={selectedParts}
+          onRemovePart={removePart}
+          onSelectSupplierPart={selectSupplierPart}
+          onSelectPurchaseOrder={selectPurchaseOrder}
+        />
+      );
+    },
+    [selectedParts]
+  );
+
+  const canStepForward = useCallback(
+    (step: number): boolean => {
+      if (!selectedParts?.length) {
+        wizard.setError(t`No parts selected`);
+        wizard.setErrorDetail(t`You must select at least one part to order`);
+        return false;
+      }
+
+      let result = true;
+      const records = [...selectedParts];
+
+      // Check for errors in each part
+      selectedParts.forEach((record: PartOrderRecord, index: number) => {
+        records[index].errors = {
+          supplier_part: !record.supplier_part
+            ? t`Supplier part is required`
+            : null,
+          quantity:
+            !record.quantity || record.quantity <= 0
+              ? t`Quantity is required`
+              : null
+        };
+
+        // If any errors are found, set the result to false
+        if (Object.values(records[index].errors).some((error) => error)) {
+          result = false;
+        }
+      });
+
+      setSelectedParts(records);
+
+      if (!result) {
+        wizard.setError(t`Invalid part selection`);
+        wizard.setErrorDetail(
+          t`Please correct the errors in the selected parts`
+        );
+      }
+
+      return result;
+    },
+    [selectedParts]
+  );
+
+  // Create the wizard manager
+  const wizard = useWizard({
+    title: t`Order Parts`,
+    steps: [],
+    renderStep: renderStep,
+    canStepForward: canStepForward
+  });
+
+  // Reset the wizard to a known state when opened
+  useEffect(() => {
+    const records: PartOrderRecord[] = [];
+
+    if (wizard.opened) {
+      parts
+        .filter((part) => part.purchaseable && part.active)
+        .forEach((part) => {
+          // Prevent duplicate entries based on pk
+          if (
+            !records.find(
+              (record: PartOrderRecord) => record.part?.pk === part.pk
+            )
+          ) {
+            records.push({
+              part: part,
+              supplier_part: undefined,
+              purchase_order: undefined,
+              quantity: 1,
+              errors: {}
+            });
+          }
+        });
+
+      setSelectedParts(records);
+    } else {
+      setSelectedParts([]);
+    }
+  }, [wizard.opened]);
+
+  return wizard;
+}
diff --git a/src/frontend/src/components/wizards/WizardDrawer.tsx b/src/frontend/src/components/wizards/WizardDrawer.tsx
new file mode 100644
index 0000000000..beec14c864
--- /dev/null
+++ b/src/frontend/src/components/wizards/WizardDrawer.tsx
@@ -0,0 +1,188 @@
+import { t } from '@lingui/macro';
+import {
+  ActionIcon,
+  Card,
+  Divider,
+  Drawer,
+  Group,
+  Paper,
+  Space,
+  Stack,
+  Stepper,
+  Tooltip
+} from '@mantine/core';
+import {
+  IconArrowLeft,
+  IconArrowRight,
+  IconCircleCheck
+} from '@tabler/icons-react';
+import { type ReactNode, useCallback, useMemo } from 'react';
+import { Boundary } from '../Boundary';
+import { StylishText } from '../items/StylishText';
+
+/**
+ * Progress stepper displayed at the top of the wizard drawer.
+ */
+function WizardProgressStepper({
+  currentStep,
+  steps,
+  onSelectStep
+}: {
+  currentStep: number;
+  steps: string[];
+  onSelectStep: (step: number) => void;
+}) {
+  if (!steps || steps.length == 0) {
+    return null;
+  }
+
+  // Determine if the user can select a particular step
+  const canSelectStep = useCallback(
+    (step: number) => {
+      if (!steps || steps.length <= 1) {
+        return false;
+      }
+
+      // Only allow single-step progression
+      return Math.abs(step - currentStep) == 1;
+    },
+    [currentStep, steps]
+  );
+
+  const canStepBackward = currentStep > 0;
+  const canStepForward = currentStep < steps.length - 1;
+
+  return (
+    <Card p='xs' withBorder>
+      <Group justify='space-between' gap='xs' wrap='nowrap'>
+        <Tooltip
+          label={steps[currentStep - 1]}
+          position='top'
+          disabled={!canStepBackward}
+        >
+          <ActionIcon
+            variant='transparent'
+            onClick={() => onSelectStep(currentStep - 1)}
+            disabled={!canStepBackward}
+          >
+            <IconArrowLeft />
+          </ActionIcon>
+        </Tooltip>
+        <Stepper
+          active={currentStep}
+          onStepClick={(stepIndex: number) => onSelectStep(stepIndex)}
+          iconSize={20}
+          size='xs'
+        >
+          {steps.map((step: string, idx: number) => (
+            <Stepper.Step
+              label={step}
+              key={step}
+              aria-label={`wizard-step-${idx}`}
+              allowStepSelect={canSelectStep(idx)}
+            />
+          ))}
+        </Stepper>
+        {canStepForward ? (
+          <Tooltip
+            label={steps[currentStep + 1]}
+            position='top'
+            disabled={!canStepForward}
+          >
+            <ActionIcon
+              variant='transparent'
+              onClick={() => onSelectStep(currentStep + 1)}
+              disabled={!canStepForward}
+            >
+              <IconArrowRight />
+            </ActionIcon>
+          </Tooltip>
+        ) : (
+          <Tooltip label={t`Complete`} position='top'>
+            <ActionIcon color='green' variant='transparent'>
+              <IconCircleCheck />
+            </ActionIcon>
+          </Tooltip>
+        )}
+      </Group>
+    </Card>
+  );
+}
+
+/**
+ * A generic "wizard" drawer, for handling multi-step processes.
+ */
+export default function WizardDrawer({
+  title,
+  currentStep,
+  steps,
+  children,
+  opened,
+  onClose,
+  onNextStep,
+  onPreviousStep
+}: {
+  title: string;
+  currentStep: number;
+  steps: string[];
+  children: ReactNode;
+  opened: boolean;
+  onClose: () => void;
+  onNextStep?: () => void;
+  onPreviousStep?: () => void;
+}) {
+  const titleBlock: ReactNode = useMemo(() => {
+    return (
+      <Stack gap='xs' style={{ width: '100%' }}>
+        <Group
+          gap='xs'
+          wrap='nowrap'
+          justify='space-between'
+          grow
+          preventGrowOverflow={false}
+        >
+          <StylishText size='xl'>{title}</StylishText>
+          <WizardProgressStepper
+            currentStep={currentStep}
+            steps={steps}
+            onSelectStep={(step: number) => {
+              if (step < currentStep) {
+                onPreviousStep?.();
+              } else {
+                onNextStep?.();
+              }
+            }}
+          />
+          <Space />
+        </Group>
+        <Divider />
+      </Stack>
+    );
+  }, [title, currentStep, steps]);
+
+  return (
+    <Drawer
+      position='bottom'
+      size={'75%'}
+      title={titleBlock}
+      withCloseButton={true}
+      closeOnEscape={false}
+      closeOnClickOutside={false}
+      styles={{
+        header: {
+          width: '100%'
+        },
+        title: {
+          width: '100%'
+        }
+      }}
+      opened={opened}
+      onClose={onClose}
+    >
+      <Boundary label='wizard-drawer'>
+        <Paper p='md'>{}</Paper>
+        {children}
+      </Boundary>
+    </Drawer>
+  );
+}
diff --git a/src/frontend/src/forms/CompanyForms.tsx b/src/frontend/src/forms/CompanyForms.tsx
index 85138ec7f3..ad9a3d63c9 100644
--- a/src/frontend/src/forms/CompanyForms.tsx
+++ b/src/frontend/src/forms/CompanyForms.tsx
@@ -18,11 +18,18 @@ import type {
 /**
  * Field set for SupplierPart instance
  */
-export function useSupplierPartFields() {
+export function useSupplierPartFields({
+  partId
+}: {
+  partId?: number;
+}) {
   return useMemo(() => {
     const fields: ApiFormFieldSet = {
       part: {
+        value: partId,
+        disabled: !!partId,
         filters: {
+          part: partId,
           purchaseable: true,
           active: true
         }
@@ -63,7 +70,7 @@ export function useSupplierPartFields() {
     };
 
     return fields;
-  }, []);
+  }, [partId]);
 }
 
 export function useManufacturerPartFields() {
diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx
index b36145dcb2..8523a3a28f 100644
--- a/src/frontend/src/forms/PurchaseOrderForms.tsx
+++ b/src/frontend/src/forms/PurchaseOrderForms.tsx
@@ -139,8 +139,10 @@ export function usePurchaseOrderLineItemFields({
  * Construct a set of fields for creating / editing a PurchaseOrder instance
  */
 export function usePurchaseOrderFields({
+  supplierId,
   duplicateOrderId
 }: {
+  supplierId?: number;
   duplicateOrderId?: number;
 }): ApiFormFieldSet {
   return useMemo(() => {
@@ -150,7 +152,8 @@ export function usePurchaseOrderFields({
       },
       description: {},
       supplier: {
-        disabled: duplicateOrderId !== undefined,
+        value: supplierId,
+        disabled: !!duplicateOrderId || !!supplierId,
         filters: {
           is_supplier: true,
           active: true
@@ -213,7 +216,7 @@ export function usePurchaseOrderFields({
     }
 
     return fields;
-  }, [duplicateOrderId]);
+  }, [duplicateOrderId, supplierId]);
 }
 
 /**
diff --git a/src/frontend/src/hooks/UseWizard.tsx b/src/frontend/src/hooks/UseWizard.tsx
new file mode 100644
index 0000000000..e8f06f9953
--- /dev/null
+++ b/src/frontend/src/hooks/UseWizard.tsx
@@ -0,0 +1,133 @@
+import { Alert, Stack } from '@mantine/core';
+import { IconExclamationCircle } from '@tabler/icons-react';
+import {
+  type ReactNode,
+  useCallback,
+  useEffect,
+  useMemo,
+  useState
+} from 'react';
+import WizardDrawer from '../components/wizards/WizardDrawer';
+
+export interface WizardProps {
+  title: string;
+  steps: string[];
+  renderStep: (step: number) => ReactNode;
+  canStepForward?: (step: number) => boolean;
+  canStepBackward?: (step: number) => boolean;
+}
+
+export interface WizardState {
+  opened: boolean;
+  currentStep: number;
+  clearError: () => void;
+  error: string | null;
+  setError: (error: string | null) => void;
+  errorDetail: string | null;
+  setErrorDetail: (errorDetail: string | null) => void;
+  openWizard: () => void;
+  closeWizard: () => void;
+  nextStep: () => void;
+  previousStep: () => void;
+  wizard: ReactNode;
+}
+
+/**
+ * Hook for managing a wizard-style multi-step process.
+ * - Manage the current step of the wizard
+ * - Allows opening and closing the wizard
+ * - Handles progression between steps with optional validation
+ */
+export default function useWizard(props: WizardProps): WizardState {
+  const [currentStep, setCurrentStep] = useState(0);
+  const [opened, setOpened] = useState(false);
+
+  const [error, setError] = useState<string | null>(null);
+  const [errorDetail, setErrorDetail] = useState<string | null>(null);
+
+  const clearError = useCallback(() => {
+    setError(null);
+    setErrorDetail(null);
+  }, []);
+
+  // Reset the wizard to an initial state when opened
+  useEffect(() => {
+    if (opened) {
+      setCurrentStep(0);
+      clearError();
+    }
+  }, [opened]);
+
+  // Open the wizard
+  const openWizard = useCallback(() => {
+    setOpened(true);
+  }, []);
+
+  // Close the wizard
+  const closeWizard = useCallback(() => {
+    setOpened(false);
+  }, []);
+
+  // Progress the wizard to the next step
+  const nextStep = useCallback(() => {
+    if (props.canStepForward && !props.canStepForward(currentStep)) {
+      return;
+    }
+
+    if (props.steps && currentStep < props.steps.length - 1) {
+      setCurrentStep(currentStep + 1);
+      clearError();
+    }
+  }, [currentStep, props.canStepForward]);
+
+  // Go back to the previous step
+  const previousStep = useCallback(() => {
+    if (props.canStepBackward && !props.canStepBackward(currentStep)) {
+      return;
+    }
+
+    if (currentStep > 0) {
+      setCurrentStep(currentStep - 1);
+      clearError();
+    }
+  }, [currentStep, props.canStepBackward]);
+
+  // Render the wizard contents for the current step
+  const contents = useMemo(() => {
+    return props.renderStep(currentStep);
+  }, [opened, currentStep, props.renderStep]);
+
+  return {
+    currentStep,
+    opened,
+    clearError,
+    error,
+    setError,
+    errorDetail,
+    setErrorDetail,
+    openWizard,
+    closeWizard,
+    nextStep,
+    previousStep,
+    wizard: (
+      <WizardDrawer
+        title={props.title}
+        currentStep={currentStep}
+        steps={props.steps}
+        opened={opened}
+        onClose={closeWizard}
+        onNextStep={nextStep}
+        onPreviousStep={previousStep}
+      >
+        <Stack gap='xs'>
+          {error && (
+            <Alert color='red' title={error} icon={<IconExclamationCircle />}>
+              {errorDetail}
+            </Alert>
+          )}
+          {contents}
+        </Stack>
+      </WizardDrawer>
+    )
+  };
+}
diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx
index 231520f2be..2ccc9763dd 100644
--- a/src/frontend/src/pages/company/SupplierPartDetail.tsx
+++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx
@@ -318,7 +318,7 @@ export default function SupplierPartDetail() {
     ];
   }, [user, supplierPart]);
 
-  const supplierPartFields = useSupplierPartFields();
+  const supplierPartFields = useSupplierPartFields({});
 
   const editSupplierPart = useEditApiFormModal({
     url: ApiEndpoints.supplier_part_list,
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index cf12fb768c..d47191501f 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -29,7 +29,7 @@ import {
   IconTruckReturn,
   IconVersions
 } from '@tabler/icons-react';
-import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
+import { useQuery } from '@tanstack/react-query';
 import { type ReactNode, useMemo, useState } from 'react';
 import { useNavigate, useParams } from 'react-router-dom';
 import Select from 'react-select';
@@ -62,6 +62,7 @@ import NotesPanel from '../../components/panels/NotesPanel';
 import type { PanelType } from '../../components/panels/Panel';
 import { PanelGroup } from '../../components/panels/PanelGroup';
 import { RenderPart } from '../../components/render/Part';
+import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
 import { formatPriceRange } from '../../defaults/formatters';
 import { ApiEndpoints } from '../../enums/ApiEndpoints';
 import { ModelType } from '../../enums/ModelType';
@@ -416,41 +417,13 @@ export default function PartDetail() {
     ];
 
     // Add in price range data
-    if (id) {
+    if (part.pricing_min || part.pricing_max) {
       br.push({
         type: 'string',
         name: 'pricing',
         label: t`Price Range`,
         value_formatter: () => {
-          const { data } = useSuspenseQuery({
-            queryKey: ['pricing', id],
-            queryFn: async () => {
-              const url = apiUrl(ApiEndpoints.part_pricing, null, {
-                id: id
-              });
-
-              return api
-                .get(url)
-                .then((response) => {
-                  switch (response.status) {
-                    case 200:
-                      return response.data;
-                    default:
-                      return {};
-                  }
-                })
-                .catch(() => {
-                  return {};
-                });
-            }
-          });
-
-          return (
-            data.overall_min &&
-            `${formatPriceRange(data.overall_min, data.overall_max)}${
-              part.units && ` / ${part.units}`
-            }`
-          );
+          return formatPriceRange(part.pricing_min, part.pricing_max);
         }
       });
     }
@@ -463,79 +436,6 @@ export default function PartDetail() {
       icon: 'serial'
     });
 
-    // Add in stocktake information
-    if (id && part.last_stocktake) {
-      br.push({
-        type: 'string',
-        name: 'stocktake',
-        label: t`Last Stocktake`,
-        unit: true,
-        value_formatter: () => {
-          const { data } = useSuspenseQuery({
-            queryKey: ['stocktake', id],
-            queryFn: async () => {
-              const url = apiUrl(ApiEndpoints.part_stocktake_list);
-
-              return api
-                .get(url, { params: { part: id, ordering: 'date' } })
-                .then((response) => {
-                  switch (response.status) {
-                    case 200:
-                      if (response.data.length > 0) {
-                        return response.data[response.data.length - 1];
-                      } else {
-                        return {};
-                      }
-                    default:
-                      return {};
-                  }
-                })
-                .catch(() => {
-                  return {};
-                });
-            }
-          });
-
-          if (data?.quantity) {
-            return `${data.quantity} (${data.date})`;
-          } else {
-            return '-';
-          }
-        }
-      });
-
-      br.push({
-        type: 'string',
-        name: 'stocktake_user',
-        label: t`Stocktake By`,
-        badge: 'user',
-        icon: 'user',
-        value_formatter: () => {
-          const { data } = useSuspenseQuery({
-            queryKey: ['stocktake', id],
-            queryFn: async () => {
-              const url = apiUrl(ApiEndpoints.part_stocktake_list);
-
-              return api
-                .get(url, { params: { part: id, ordering: 'date' } })
-                .then((response) => {
-                  switch (response.status) {
-                    case 200:
-                      return response.data[response.data.length - 1];
-                    default:
-                      return {};
-                  }
-                })
-                .catch(() => {
-                  return {};
-                });
-            }
-          });
-          return data?.user;
-        }
-      });
-    }
-
     return part ? (
       <ItemDetailsGrid>
         <Grid>
@@ -565,7 +465,14 @@ export default function PartDetail() {
     ) : (
       <Skeleton />
     );
-  }, [globalSettings, part, serials, instanceQuery]);
+  }, [
+    globalSettings,
+    part,
+    id,
+    serials,
+    instanceQuery.isFetching,
+    instanceQuery.data
+  ]);
 
   // Part data panels (recalculate when part data changes)
   const partPanels: PanelType[] = useMemo(() => {
@@ -735,7 +642,7 @@ export default function PartDetail() {
         model_id: part?.pk
       })
     ];
-  }, [id, part, user, globalSettings, userSettings]);
+  }, [id, part, user, globalSettings, userSettings, detailsPanel]);
 
   // Fetch information on part revision
   const partRevisionQuery = useQuery({
@@ -820,19 +727,18 @@ export default function PartDetail() {
     });
   }, [part, partRevisionQuery.isFetching, partRevisionQuery.data]);
 
-  const breadcrumbs = useMemo(
-    () => [
+  const breadcrumbs = useMemo(() => {
+    return [
       { name: t`Parts`, url: '/part' },
       ...(part.category_path ?? []).map((c: any) => ({
         name: c.name,
         url: getDetailUrl(ModelType.partcategory, c.pk)
       }))
-    ],
-    [part]
-  );
+    ];
+  }, [part]);
 
   const badges: ReactNode[] = useMemo(() => {
-    if (instanceQuery.isLoading || instanceQuery.isFetching) {
+    if (instanceQuery.isFetching) {
       return [];
     }
 
@@ -883,7 +789,7 @@ export default function PartDetail() {
         key='inactive'
       />
     ];
-  }, [part, instanceQuery]);
+  }, [part, instanceQuery.isFetching]);
 
   const partFields = usePartFields({ create: false });
 
@@ -970,6 +876,10 @@ export default function PartDetail() {
   const countStockItems = useCountStockItem(stockActionProps);
   const transferStockItems = useTransferStockItem(stockActionProps);
 
+  const orderPartsWizard = OrderPartsWizard({
+    parts: [part]
+  });
+
   const partActions = useMemo(() => {
     return [
       <AdminButton model={ModelType.part} id={part.pk} />,
@@ -1011,6 +921,18 @@ export default function PartDetail() {
             onClick: () => {
               part.pk && transferStockItems.open();
             }
+          },
+          {
+            name: t`Order`,
+            tooltip: t`Order Stock`,
+            hidden:
+              !user.hasAddRole(UserRoles.purchase_order) ||
+              !part?.active ||
+              !part?.purchaseable,
+            icon: <IconShoppingCart color='blue' />,
+            onClick: () => {
+              orderPartsWizard.openWizard();
+            }
           }
         ]}
       />,
@@ -1047,6 +969,7 @@ export default function PartDetail() {
       {duplicatePart.modal}
       {editPart.modal}
       {deletePart.modal}
+      {orderPartsWizard.wizard}
       <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
         <Stack gap='xs'>
           <NavigationTree
diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx
index b9aedbcfa7..c362501458 100644
--- a/src/frontend/src/pages/stock/StockDetail.tsx
+++ b/src/frontend/src/pages/stock/StockDetail.tsx
@@ -7,6 +7,7 @@ import {
   IconHistory,
   IconInfoCircle,
   IconPackages,
+  IconShoppingCart,
   IconSitemap
 } from '@tabler/icons-react';
 import { useQuery } from '@tanstack/react-query';
@@ -41,6 +42,7 @@ import type { PanelType } from '../../components/panels/Panel';
 import { PanelGroup } from '../../components/panels/PanelGroup';
 import LocateItemButton from '../../components/plugins/LocateItemButton';
 import { StatusRenderer } from '../../components/render/StatusRenderer';
+import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
 import { formatCurrency } from '../../defaults/formatters';
 import { ApiEndpoints } from '../../enums/ApiEndpoints';
 import { ModelType } from '../../enums/ModelType';
@@ -354,7 +356,7 @@ export default function StockDetail() {
         <DetailsTable fields={br} item={data} />
       </ItemDetailsGrid>
     );
-  }, [stockitem, instanceQuery, enableExpiry]);
+  }, [stockitem, instanceQuery.isFetching, enableExpiry]);
 
   const showBuildAllocations: boolean = useMemo(() => {
     // Determine if "build allocations" should be shown for this stock item
@@ -652,6 +654,10 @@ export default function StockDetail() {
     }
   });
 
+  const orderPartsWizard = OrderPartsWizard({
+    parts: stockitem.part_detail ? [stockitem.part_detail] : []
+  });
+
   const stockActions = useMemo(() => {
     const inStock =
       user.hasChangeRole(UserRoles.stock) &&
@@ -717,6 +723,17 @@ export default function StockDetail() {
               stockitem.pk && removeStockItem.open();
             }
           },
+          {
+            name: t`Transfer`,
+            tooltip: t`Transfer Stock`,
+            hidden: !inStock,
+            icon: (
+              <InvenTreeIcon icon='transfer' iconProps={{ color: 'blue' }} />
+            ),
+            onClick: () => {
+              stockitem.pk && transferStockItem.open();
+            }
+          },
           {
             name: t`Serialize`,
             tooltip: t`Serialize stock`,
@@ -730,14 +747,15 @@ export default function StockDetail() {
             }
           },
           {
-            name: t`Transfer`,
-            tooltip: t`Transfer Stock`,
-            hidden: !inStock,
-            icon: (
-              <InvenTreeIcon icon='transfer' iconProps={{ color: 'blue' }} />
-            ),
+            name: t`Order`,
+            tooltip: t`Order Stock`,
+            hidden:
+              !user.hasAddRole(UserRoles.purchase_order) ||
+              !stockitem.part_detail?.active ||
+              !stockitem.part_detail?.purchaseable,
+            icon: <IconShoppingCart color='blue' />,
             onClick: () => {
-              stockitem.pk && transferStockItem.open();
+              orderPartsWizard.openWizard();
             }
           },
           {
@@ -898,6 +916,7 @@ export default function StockDetail() {
         {serializeStockItem.modal}
         {returnStockItem.modal}
         {assignToCustomer.modal}
+        {orderPartsWizard.wizard}
       </Stack>
     </InstanceDetail>
   );
diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx
index d7266dbc4a..76bc6b860e 100644
--- a/src/frontend/src/tables/build/BuildLineTable.tsx
+++ b/src/frontend/src/tables/build/BuildLineTable.tsx
@@ -13,6 +13,7 @@ import { useNavigate } from 'react-router-dom';
 
 import { ActionButton } from '../../components/buttons/ActionButton';
 import { ProgressBar } from '../../components/items/ProgressBar';
+import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
 import { ApiEndpoints } from '../../enums/ApiEndpoints';
 import { ModelType } from '../../enums/ModelType';
 import { UserRoles } from '../../enums/Roles';
@@ -20,7 +21,6 @@ import {
   useAllocateStockToBuildForm,
   useBuildOrderFields
 } from '../../forms/BuildForms';
-import { notYetImplemented } from '../../functions/notifications';
 import {
   useCreateApiFormModal,
   useDeleteApiFormModal,
@@ -527,6 +527,12 @@ export default function BuildLineTable({
     table: table
   });
 
+  const [partsToOrder, setPartsToOrder] = useState<any[]>([]);
+
+  const orderPartsWizard = OrderPartsWizard({
+    parts: partsToOrder
+  });
+
   const rowActions = useCallback(
     (record: any): RowAction[] => {
       const part = record.part_detail ?? {};
@@ -552,7 +558,6 @@ export default function BuildLineTable({
         record.trackable == hasOutput;
 
       const canOrder =
-        in_production &&
         !consumable &&
         user.hasAddRole(UserRoles.purchase_order) &&
         part.purchaseable;
@@ -588,8 +593,12 @@ export default function BuildLineTable({
           icon: <IconShoppingCart />,
           title: t`Order Stock`,
           hidden: !canOrder,
+          disabled: !table.hasSelectedRecords,
           color: 'blue',
-          onClick: notYetImplemented
+          onClick: () => {
+            setPartsToOrder([record.part_detail]);
+            orderPartsWizard.openWizard();
+          }
         },
         {
           icon: <IconTool />,
@@ -631,6 +640,24 @@ export default function BuildLineTable({
           autoAllocateStock.open();
         }}
       />,
+      <ActionButton
+        key='order-parts'
+        hidden={!user.hasAddRole(UserRoles.purchase_order)}
+        disabled={!table.hasSelectedRecords}
+        icon={<IconShoppingCart />}
+        color='blue'
+        tooltip={t`Order Parts`}
+        onClick={() => {
+          setPartsToOrder(
+            table.selectedRecords
+              .filter(
+                (r) => r.part_detail?.purchaseable && r.part_detail?.active
+              )
+              .map((r) => r.part_detail)
+          );
+          orderPartsWizard.openWizard();
+        }}
+      />,
       <ActionButton
         key='allocate-stock'
         icon={<IconArrowRight />}
@@ -749,6 +776,7 @@ export default function BuildLineTable({
       {deallocateStock.modal}
       {editAllocation.modal}
       {deleteAllocation.modal}
+      {orderPartsWizard.wizard}
       <InvenTreeTable
         url={apiUrl(ApiEndpoints.build_line_list)}
         tableState={table}
diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx
index 51583ce1f3..f3c83e5d01 100644
--- a/src/frontend/src/tables/part/PartTable.tsx
+++ b/src/frontend/src/tables/part/PartTable.tsx
@@ -2,12 +2,16 @@ import { t } from '@lingui/macro';
 import { Group, Text } from '@mantine/core';
 import { type ReactNode, useMemo } from 'react';
 
+import { IconShoppingCart } from '@tabler/icons-react';
 import { AddItemButton } from '../../components/buttons/AddItemButton';
+import { ActionDropdown } from '../../components/items/ActionDropdown';
+import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
 import { formatPriceRange } from '../../defaults/formatters';
 import { ApiEndpoints } from '../../enums/ApiEndpoints';
 import { ModelType } from '../../enums/ModelType';
 import { UserRoles } from '../../enums/Roles';
 import { usePartFields } from '../../forms/PartForms';
+import { InvenTreeIcon } from '../../functions/icons';
 import { useCreateApiFormModal } from '../../hooks/UseForm';
 import { useTable } from '../../hooks/UseTable';
 import { apiUrl } from '../../states/ApiState';
@@ -333,8 +337,25 @@ export function PartListTable({
     modelType: ModelType.part
   });
 
+  const orderPartsWizard = OrderPartsWizard({ parts: table.selectedRecords });
+
   const tableActions = useMemo(() => {
     return [
+      <ActionDropdown
+        tooltip={t`Part Actions`}
+        icon={<InvenTreeIcon icon='part' />}
+        disabled={!table.hasSelectedRecords}
+        actions={[
+          {
+            name: t`Order Parts`,
+            icon: <IconShoppingCart color='blue' />,
+            tooltip: t`Order selected parts`,
+            onClick: () => {
+              orderPartsWizard.openWizard();
+            }
+          }
+        ]}
+      />,
       <AddItemButton
         key='add-part'
         hidden={!user.hasAddRole(UserRoles.part)}
@@ -342,11 +363,12 @@ export function PartListTable({
         onClick={() => newPart.open()}
       />
     ];
-  }, [user]);
+  }, [user, table.hasSelectedRecords]);
 
   return (
     <>
       {newPart.modal}
+      {orderPartsWizard.wizard}
       <InvenTreeTable
         url={apiUrl(ApiEndpoints.part_list)}
         tableState={table}
diff --git a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx
index f8219cdf8f..5d4c65c022 100644
--- a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx
+++ b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx
@@ -158,7 +158,9 @@ export function SupplierPartTable({
     ];
   }, [params]);
 
-  const supplierPartFields = useSupplierPartFields();
+  const supplierPartFields = useSupplierPartFields({
+    partId: params?.part
+  });
 
   const addSupplierPart = useCreateApiFormModal({
     url: ApiEndpoints.supplier_part_list,
@@ -208,7 +210,7 @@ export function SupplierPartTable({
     ];
   }, []);
 
-  const editSupplierPartFields = useSupplierPartFields();
+  const editSupplierPartFields = useSupplierPartFields({});
 
   const [selectedSupplierPart, setSelectedSupplierPart] = useState<number>(0);
 
diff --git a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
index 2fb3cda56b..77841f556d 100644
--- a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
+++ b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
@@ -14,6 +14,7 @@ import { useNavigate } from 'react-router-dom';
 import { ActionButton } from '../../components/buttons/ActionButton';
 import { AddItemButton } from '../../components/buttons/AddItemButton';
 import { ProgressBar } from '../../components/items/ProgressBar';
+import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
 import { formatCurrency } from '../../defaults/formatters';
 import { ApiEndpoints } from '../../enums/ApiEndpoints';
 import { ModelType } from '../../enums/ModelType';
@@ -24,7 +25,6 @@ import {
   useSalesOrderAllocateSerialsFields,
   useSalesOrderLineItemFields
 } from '../../forms/SalesOrderForms';
-import { notYetImplemented } from '../../functions/notifications';
 import {
   useCreateApiFormModal,
   useDeleteApiFormModal,
@@ -285,6 +285,12 @@ export default function SalesOrderLineItemTable({
     }
   });
 
+  const [partsToOrder, setPartsToOrder] = useState<any[]>([]);
+
+  const orderPartsWizard = OrderPartsWizard({
+    parts: partsToOrder
+  });
+
   const tableFilters: TableFilter[] = useMemo(() => {
     return [
       {
@@ -313,6 +319,18 @@ export default function SalesOrderLineItemTable({
         }}
         hidden={!editable || !user.hasAddRole(UserRoles.sales_order)}
       />,
+      <ActionButton
+        key='order-parts'
+        hidden={!user.hasAddRole(UserRoles.purchase_order)}
+        disabled={!table.hasSelectedRecords}
+        tooltip={t`Order Parts`}
+        icon={<IconShoppingCart />}
+        color='blue'
+        onClick={() => {
+          setPartsToOrder(table.selectedRecords.map((r) => r.part_detail));
+          orderPartsWizard.openWizard();
+        }}
+      />,
       <ActionButton
         key='allocate-stock'
         tooltip={t`Allocate Stock`}
@@ -396,7 +414,10 @@ export default function SalesOrderLineItemTable({
           title: t`Order stock`,
           icon: <IconShoppingCart />,
           color: 'blue',
-          onClick: notYetImplemented
+          onClick: () => {
+            setPartsToOrder([record.part_detail]);
+            orderPartsWizard.openWizard();
+          }
         },
         RowEditAction({
           hidden: !editable || !user.hasChangeRole(UserRoles.sales_order),
@@ -455,6 +476,7 @@ export default function SalesOrderLineItemTable({
       {newBuildOrder.modal}
       {allocateBySerials.modal}
       {allocateStock.modal}
+      {orderPartsWizard.wizard}
       <InvenTreeTable
         url={apiUrl(ApiEndpoints.sales_order_line_list)}
         tableState={table}
diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx
index d9dfc20cf4..19ccc52c3e 100644
--- a/src/frontend/src/tables/stock/StockItemTable.tsx
+++ b/src/frontend/src/tables/stock/StockItemTable.tsx
@@ -1,9 +1,10 @@
 import { t } from '@lingui/macro';
 import { Group, Text } from '@mantine/core';
-import { type ReactNode, useMemo } from 'react';
+import { type ReactNode, useMemo, useState } from 'react';
 
 import { AddItemButton } from '../../components/buttons/AddItemButton';
 import { ActionDropdown } from '../../components/items/ActionDropdown';
+import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
 import { formatCurrency, formatPriceRange } from '../../defaults/formatters';
 import { ApiEndpoints } from '../../enums/ApiEndpoints';
 import { ModelType } from '../../enums/ModelType';
@@ -21,7 +22,6 @@ import {
   useTransferStockItem
 } from '../../forms/StockForms';
 import { InvenTreeIcon } from '../../functions/icons';
-import { notYetImplemented } from '../../functions/notifications';
 import { useCreateApiFormModal } from '../../hooks/UseForm';
 import { useTable } from '../../hooks/UseTable';
 import { apiUrl } from '../../states/ApiState';
@@ -540,6 +540,12 @@ export function StockItemTable({
     modelType: ModelType.stockitem
   });
 
+  const [partsToOrder, setPartsToOrder] = useState<any[]>([]);
+
+  const orderPartsWizard = OrderPartsWizard({
+    parts: partsToOrder
+  });
+
   const transferStock = useTransferStockItem(tableActionParams);
   const addStock = useAddStockItem(tableActionParams);
   const removeStock = useRemoveStockItem(tableActionParams);
@@ -562,6 +568,17 @@ export function StockItemTable({
         icon={<InvenTreeIcon icon='stock' />}
         disabled={table.selectedRecords.length === 0}
         actions={[
+          {
+            name: t`Count Stock`,
+            icon: (
+              <InvenTreeIcon icon='stocktake' iconProps={{ color: 'blue' }} />
+            ),
+            tooltip: t`Count Stock`,
+            disabled: !can_add_stocktake,
+            onClick: () => {
+              countStock.open();
+            }
+          },
           {
             name: t`Add Stock`,
             icon: <InvenTreeIcon icon='add' iconProps={{ color: 'green' }} />,
@@ -580,17 +597,6 @@ export function StockItemTable({
               removeStock.open();
             }
           },
-          {
-            name: t`Count Stock`,
-            icon: (
-              <InvenTreeIcon icon='stocktake' iconProps={{ color: 'blue' }} />
-            ),
-            tooltip: t`Count Stock`,
-            disabled: !can_add_stocktake,
-            onClick: () => {
-              countStock.open();
-            }
-          },
           {
             name: t`Transfer Stock`,
             icon: (
@@ -624,8 +630,14 @@ export function StockItemTable({
             name: t`Order stock`,
             icon: <InvenTreeIcon icon='buy' />,
             tooltip: t`Order new stock`,
-            disabled: !can_add_order || !can_change_order,
-            onClick: notYetImplemented
+            hidden: !user.hasAddRole(UserRoles.purchase_order),
+            disabled: !table.hasSelectedRecords,
+            onClick: () => {
+              setPartsToOrder(
+                table.selectedRecords.map((record) => record.part_detail)
+              );
+              orderPartsWizard.openWizard();
+            }
           },
           {
             name: t`Assign to customer`,
@@ -654,7 +666,7 @@ export function StockItemTable({
         onClick={() => newStockItem.open()}
       />
     ];
-  }, [user, table, allowAdd]);
+  }, [user, allowAdd, table.hasSelectedRecords, table.selectedRecords]);
 
   return (
     <>
@@ -667,6 +679,7 @@ export function StockItemTable({
       {mergeStock.modal}
       {assignStock.modal}
       {deleteStock.modal}
+      {orderPartsWizard.wizard}
       <InvenTreeTable
         url={apiUrl(ApiEndpoints.stock_item_list)}
         tableState={table}
diff --git a/src/frontend/tests/pages/pui_purchase_order.spec.ts b/src/frontend/tests/pages/pui_purchase_order.spec.ts
index ff8207346d..90d753c7dc 100644
--- a/src/frontend/tests/pages/pui_purchase_order.spec.ts
+++ b/src/frontend/tests/pages/pui_purchase_order.spec.ts
@@ -1,4 +1,5 @@
 import { test } from '../baseFixtures.ts';
+import { baseUrl } from '../defaults.ts';
 import { clickButtonIfVisible, openFilterDrawer } from '../helpers.ts';
 import { doQuickLogin } from '../login.ts';
 
@@ -76,6 +77,83 @@ test('Purchase Orders - Filters', async ({ page }) => {
   await page.getByRole('option', { name: 'Target Date After' }).waitFor();
 });
 
+test('Purchase Orders - Order Parts', async ({ page }) => {
+  await doQuickLogin(page);
+
+  // Open "Order Parts" wizard from the "parts" table
+  await page.getByRole('tab', { name: 'Parts' }).click();
+  await page
+    .getByLabel('panel-tabs-partcategory')
+    .getByRole('tab', { name: 'Parts' })
+    .click();
+
+  // Select multiple parts
+  for (let ii = 1; ii < 5; ii++) {
+    await page.getByLabel(`Select record ${ii}`, { exact: true }).click();
+  }
+
+  await page.getByLabel('action-menu-part-actions').click();
+  await page.getByLabel('action-menu-part-actions-order-parts').click();
+  await page
+    .getByRole('heading', { name: 'Order Parts' })
+    .locator('div')
+    .first()
+    .waitFor();
+  await page.getByRole('banner').getByRole('button').click();
+
+  // Open "Order Parts" wizard from the "Stock Items" table
+  await page.getByRole('tab', { name: 'Stock' }).click();
+  await page.getByRole('tab', { name: 'Stock Items' }).click();
+
+  // Select multiple stock items
+  for (let ii = 2; ii < 7; ii += 2) {
+    await page.getByLabel(`Select record ${ii}`, { exact: true }).click();
+  }
+
+  await page
+    .getByLabel('Stock Items')
+    .getByLabel('action-menu-stock-actions')
+    .click();
+  await page.getByLabel('action-menu-stock-actions-order-stock').click();
+  await page.getByRole('banner').getByRole('button').click();
+
+  // Order from the part detail page
+  await page.goto(`${baseUrl}/part/69/`);
+  await page.waitForURL('**/part/69/**');
+
+  await page.getByLabel('action-menu-stock-actions').click();
+  await page.getByLabel('action-menu-stock-actions-order').click();
+
+  // Select supplier part
+  await page.getByLabel('related-field-supplier_part').click();
+  await page.getByText('WM1731-ND').click();
+
+  // Option to create a new supplier part
+  await page.getByLabel('action-button-new-supplier-part').click();
+  await page.getByLabel('related-field-supplier', { exact: true }).click();
+  await page.getByText('Future').click();
+  await page.getByRole('button', { name: 'Cancel' }).click();
+
+  // Select purchase order
+  await page.getByLabel('related-field-purchase_order').click();
+  await page.getByText('PO0001').click();
+
+  // Option to create a new purchase order
+  await page.getByLabel('action-button-new-purchase-order').click();
+  await page.getByLabel('related-field-project_code').click();
+  await page.getByText('PRJ-PHO').click();
+  await page.getByRole('button', { name: 'Cancel' }).click();
+
+  // Add the part to the purchase order
+  await page.getByLabel('action-button-add-to-selected').click();
+  await page.getByLabel('number-field-quantity').fill('100');
+  await page.waitForTimeout(250);
+  await page.getByRole('button', { name: 'Submit' }).click();
+  await page
+    .getByText('All selected parts added to a purchase order')
+    .waitFor();
+});
+
 /**
  * Tests for receiving items against a purchase order
  */