diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index af05ad088c..ef3a80ae9b 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -1,12 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 359
+INVENTREE_API_VERSION = 360
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
+v360 -> 2025-07-02 : https://github.com/inventree/InvenTree/pull/9937
+ - Provide more detailed StockItem information in the BuildItem serializer
+ - Provide more detailed StockItem information in the SalesOrderAllocation serializer
+
v359 -> 2025-07-01 : https://github.com/inventree/InvenTree/pull/9909
- Fixes annotated types for various part fields
diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py
index 18b08565f3..ba69dc5508 100644
--- a/src/backend/InvenTree/build/serializers.py
+++ b/src/backend/InvenTree/build/serializers.py
@@ -42,7 +42,7 @@ from stock.generators import generate_batch_code
from stock.models import StockItem, StockLocation
from stock.serializers import (
LocationBriefSerializer,
- StockItemSerializerBrief,
+ StockItemSerializer,
StockStatusCustomSerializer,
)
from stock.status_codes import StockStatus
@@ -1227,7 +1227,7 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
pricing=False,
)
- stock_item_detail = StockItemSerializerBrief(
+ stock_item_detail = StockItemSerializer(
source='stock_item', read_only=True, allow_null=True, label=_('Stock Item')
)
diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py
index e1bb3dfc1d..e26b9d3a91 100644
--- a/src/backend/InvenTree/order/serializers.py
+++ b/src/backend/InvenTree/order/serializers.py
@@ -1430,7 +1430,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(
source='item.part', many=False, read_only=True, allow_null=True
)
- item_detail = stock.serializers.StockItemSerializerBrief(
+ item_detail = stock.serializers.StockItemSerializer(
source='item', many=False, read_only=True, allow_null=True
)
location_detail = stock.serializers.LocationBriefSerializer(
diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py
index 9c3947d53e..d48c9371f8 100644
--- a/src/backend/InvenTree/stock/serializers.py
+++ b/src/backend/InvenTree/stock/serializers.py
@@ -307,41 +307,6 @@ class StockItemTestResultSerializer(
return data
-class StockItemSerializerBrief(
- InvenTree.serializers.NotesFieldMixin,
- InvenTree.serializers.InvenTreeModelSerializer,
-):
- """Brief serializers for a StockItem."""
-
- class Meta:
- """Metaclass options."""
-
- model = StockItem
- fields = [
- 'part',
- 'part_name',
- 'pk',
- 'location',
- 'quantity',
- 'serial',
- 'batch',
- 'supplier_part',
- 'barcode_hash',
- ]
-
- read_only_fields = ['barcode_hash']
-
- part_name = serializers.CharField(source='part.full_name', read_only=True)
-
- quantity = InvenTreeDecimalField()
-
- def validate_serial(self, value):
- """Make sure serial is not to big."""
- if abs(InvenTree.helpers.extract_int(value)) > 0x7FFFFFFF:
- raise serializers.ValidationError(_('Serial number is too large'))
- return value
-
-
@register_importer()
class StockItemSerializer(
DataImportExportSerializerMixin,
@@ -1336,7 +1301,7 @@ class StockTrackingSerializer(
label = serializers.CharField(read_only=True)
- item_detail = StockItemSerializerBrief(
+ item_detail = StockItemSerializer(
source='item', many=False, read_only=True, allow_null=True
)
diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx
index 0cb1510375..5375627fac 100644
--- a/src/frontend/src/components/items/ActionDropdown.tsx
+++ b/src/frontend/src/components/items/ActionDropdown.tsx
@@ -57,7 +57,7 @@ export function ActionDropdown({
disabled?: boolean;
hidden?: boolean;
noindicator?: boolean;
-}) {
+}): ReactNode {
const hasActions = useMemo(() => {
return actions.some((action) => !action.hidden);
}, [actions]);
diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx
index d1cbf18a9e..2a5beccd12 100644
--- a/src/frontend/src/forms/PurchaseOrderForms.tsx
+++ b/src/frontend/src/forms/PurchaseOrderForms.tsx
@@ -436,7 +436,10 @@ function LineItemFormRow({
}
// Selected location is base part's default location
- if (location === record.part_detail.default_location) {
+ if (
+ record.part_detail?.default_location &&
+ location === record.part_detail.default_location
+ ) {
return t`Default location selected`;
}
diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx
index cd393e61af..243d7b698d 100644
--- a/src/frontend/src/forms/StockForms.tsx
+++ b/src/frontend/src/forms/StockForms.tsx
@@ -934,7 +934,7 @@ function stockMergeFields(items: any[]): ApiFormFieldSet {
]
},
location: {
- default: items[0]?.part_detail.default_location,
+ default: items[0]?.part_detail?.default_location,
filters: {
structural: false
}
diff --git a/src/frontend/src/hooks/UseStockAdjustActions.tsx b/src/frontend/src/hooks/UseStockAdjustActions.tsx
new file mode 100644
index 0000000000..d6d994adbb
--- /dev/null
+++ b/src/frontend/src/hooks/UseStockAdjustActions.tsx
@@ -0,0 +1,208 @@
+import { UserRoles } from '@lib/index';
+import type { UseModalReturn } from '@lib/types/Modals';
+import { t } from '@lingui/core/macro';
+import { type ReactNode, useMemo } from 'react';
+import type { ActionDropdownItem } from '../components/items/ActionDropdown';
+import { ActionDropdown } from '../components/items/ActionDropdown';
+import {
+ type StockOperationProps,
+ useAddStockItem,
+ useAssignStockItem,
+ useChangeStockStatus,
+ useCountStockItem,
+ useDeleteStockItem,
+ useMergeStockItem,
+ useRemoveStockItem,
+ useTransferStockItem
+} from '../forms/StockForms';
+import { InvenTreeIcon } from '../functions/icons';
+import { useUserState } from '../states/UserState';
+
+interface StockAdjustActionProps {
+ formProps: StockOperationProps;
+ enabled?: boolean;
+ add?: boolean;
+ assign?: boolean;
+ count?: boolean;
+ changeStatus?: boolean;
+ delete?: boolean;
+ merge?: boolean;
+ remove?: boolean;
+ transfer?: boolean;
+}
+
+interface StockAdjustActionReturnProps {
+ modals: UseModalReturn[];
+ dropdown: ReactNode;
+ menuActions: ActionDropdownItem[];
+ hasActions: boolean;
+}
+
+/**
+ * Hook to provide an interface for stock transfer actions.
+ * - Provides a set of modals for creating, editing, and deleting stock transfers.
+ * - Provides a set of menu actions for opening the modals.
+ */
+
+export function useStockAdjustActions(
+ props: StockAdjustActionProps
+): StockAdjustActionReturnProps {
+ const user = useUserState();
+
+ // The available modals for stock adjustment actions
+ const addStock = useAddStockItem(props.formProps);
+ const assignStock = useAssignStockItem(props.formProps);
+ const countStock = useCountStockItem(props.formProps);
+ const changeStatus = useChangeStockStatus(props.formProps);
+ const deleteStock = useDeleteStockItem(props.formProps);
+ const mergeStock = useMergeStockItem(props.formProps);
+ const removeStock = useRemoveStockItem(props.formProps);
+ const transferStock = useTransferStockItem(props.formProps);
+
+ // Construct a list of modals available for stock adjustment actions
+ const modals: UseModalReturn[] = useMemo(() => {
+ const modals: UseModalReturn[] = [];
+
+ if (!user.hasChangeRole(UserRoles.stock)) {
+ return [];
+ }
+
+ props.add != false && modals.push(addStock);
+ props.assign != false && modals.push(assignStock);
+ props.count != false && modals.push(countStock);
+ props.changeStatus != false && modals.push(changeStatus);
+ props.merge != false && modals.push(mergeStock);
+ props.remove != false && modals.push(removeStock);
+ props.transfer != false && modals.push(transferStock);
+ props.delete != false &&
+ user.hasDeleteRole(UserRoles.stock) &&
+ modals.push(deleteStock);
+
+ return modals;
+ }, [props, user]);
+
+ // Construct a list of actions available in the stock adjustment dropdown menu
+ const menuActions: ActionDropdownItem[] = useMemo(() => {
+ const menuActions: ActionDropdownItem[] = [];
+
+ if (!user.hasChangeRole(UserRoles.stock)) {
+ return [];
+ }
+
+ props.count != false &&
+ menuActions.push({
+ name: t`Count Stock`,
+ icon: ,
+ tooltip: t`Count selected stock items`,
+ onClick: () => {
+ countStock.open();
+ }
+ });
+
+ props.add != false &&
+ menuActions.push({
+ name: t`Add Stock`,
+ icon: ,
+ tooltip: t`Add to selected stock items`,
+ onClick: () => {
+ addStock.open();
+ }
+ });
+
+ props.remove != false &&
+ menuActions.push({
+ name: t`Remove Stock`,
+ icon: ,
+ tooltip: t`Remove from selected stock items`,
+ onClick: () => {
+ removeStock.open();
+ }
+ });
+
+ props.transfer != false &&
+ menuActions.push({
+ name: t`Transfer Stock`,
+ icon: ,
+ tooltip: t`Transfer selected stock items`,
+ onClick: () => {
+ transferStock.open();
+ }
+ });
+
+ props.merge != false &&
+ menuActions.push({
+ name: t`Merge Stock`,
+ icon: ,
+ tooltip: t`Merge selected stock items`,
+ onClick: () => {
+ mergeStock.open();
+ }
+ });
+
+ props.changeStatus != false &&
+ menuActions.push({
+ name: t`Change Status`,
+ icon: ,
+ tooltip: t`Change status of selected stock items`,
+ onClick: () => {
+ changeStatus.open();
+ }
+ });
+
+ props.assign != false &&
+ menuActions.push({
+ name: t`Assign Stock`,
+ icon: ,
+ tooltip: t`Assign selected stock items to a customer`,
+ onClick: () => {
+ assignStock.open();
+ }
+ });
+
+ props.delete != false &&
+ menuActions.push({
+ name: t`Delete Stock`,
+ icon: ,
+ tooltip: t`Delete selected stock items`,
+ disabled: !user.hasDeleteRole(UserRoles.stock),
+ onClick: () => {
+ deleteStock.open();
+ }
+ });
+
+ return menuActions;
+ }, [props, user]);
+
+ // Determine if there are any items selected
+ const hasItems: boolean = useMemo(() => {
+ return (props.formProps?.items?.length ?? 0) > 0;
+ }, [props.formProps.items]);
+
+ // Determine if there are any actions available
+ const hasActions: boolean = useMemo(() => {
+ return menuActions.length > 0;
+ }, [menuActions]);
+
+ // Construct a dropdown menu for stock actions
+ const dropdown: ReactNode = useMemo(() => {
+ return (
+ }
+ actions={menuActions}
+ disabled={
+ props.enabled != true && (props.enabled == false || !hasItems)
+ }
+ hidden={!hasActions}
+ />
+ );
+ }, [props.enabled, hasActions, hasItems, menuActions]);
+
+ return {
+ modals,
+ menuActions,
+ hasActions,
+ dropdown
+ };
+}
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index f14f748e02..c1a23c45b9 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -72,17 +72,15 @@ import { formatPriceRange } from '../../defaults/formatters';
import { usePartFields } from '../../forms/PartForms';
import {
type StockOperationProps,
- useCountStockItem,
- useFindSerialNumberForm,
- useTransferStockItem
+ useFindSerialNumberForm
} from '../../forms/StockForms';
-import { InvenTreeIcon } from '../../functions/icons';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
+import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
import {
useGlobalSettingsState,
useUserSettingsState
@@ -899,7 +897,7 @@ export default function PartDetail() {
)
});
- const stockActionProps: StockOperationProps = useMemo(() => {
+ const stockOperationProps: StockOperationProps = useMemo(() => {
return {
pk: part.pk,
model: ModelType.part,
@@ -910,8 +908,11 @@ export default function PartDetail() {
};
}, [part]);
- const countStockItems = useCountStockItem(stockActionProps);
- const transferStockItems = useTransferStockItem(stockActionProps);
+ const stockAdjustActions = useStockAdjustActions({
+ formProps: stockOperationProps,
+ merge: false,
+ enabled: true
+ });
const orderPartsWizard = OrderPartsWizard({
parts: [part]
@@ -947,28 +948,7 @@ export default function PartDetail() {
tooltip={t`Stock Actions`}
icon={}
actions={[
- {
- icon: (
-
- ),
- name: t`Count Stock`,
- tooltip: t`Count part stock`,
- hidden: !user.hasChangeRole(UserRoles.stock),
- onClick: () => {
- part.pk && countStockItems.open();
- }
- },
- {
- icon: (
-
- ),
- name: t`Transfer Stock`,
- tooltip: t`Transfer part stock`,
- hidden: !user.hasChangeRole(UserRoles.stock),
- onClick: () => {
- part.pk && transferStockItems.open();
- }
- },
+ ...stockAdjustActions.menuActions,
{
name: t`Order`,
tooltip: t`Order Stock`,
@@ -1009,7 +989,7 @@ export default function PartDetail() {
]}
/>
];
- }, [id, part, user]);
+ }, [id, part, user, stockAdjustActions.menuActions]);
const enableRevisionSelection: boolean = useMemo(() => {
return (
@@ -1023,10 +1003,9 @@ export default function PartDetail() {
{editPart.modal}
{deletePart.modal}
{duplicatePart.modal}
- {countStockItems.modal}
{orderPartsWizard.wizard}
{findBySerialNumber.modal}
- {transferStockItems.modal}
+ {stockAdjustActions.modals.map((modal) => modal.modal)}
{user.hasViewRole(UserRoles.part_category) && (
diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx
index 6611f1b740..a762585bfd 100644
--- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx
+++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx
@@ -475,8 +475,14 @@ export default function SalesOrderDetail() {
(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;
+ const autoComplete = globalSettings.isSet('SALESORDER_SHIP_COMPLETE');
+
+ const canShip: boolean =
+ !autoComplete && canEdit && order.status == soStatus.IN_PROGRESS;
+ const canComplete: boolean =
+ canEdit &&
+ (order.status == soStatus.SHIPPED ||
+ (autoComplete && order.status == soStatus.IN_PROGRESS));
return [
];
- }, [user, order, soStatus]);
+ }, [user, order, soStatus, globalSettings]);
const orderBadges: ReactNode[] = useMemo(() => {
return instanceQuery.isLoading
diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx
index 64d9d976cd..32442a0e7b 100644
--- a/src/frontend/src/pages/stock/LocationDetail.tsx
+++ b/src/frontend/src/pages/stock/LocationDetail.tsx
@@ -18,7 +18,6 @@ import {
} from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
- ActionDropdown,
BarcodeActionDropdown,
DeleteItemAction,
EditItemAction,
@@ -33,9 +32,7 @@ import { PanelGroup } from '../../components/panels/PanelGroup';
import LocateItemButton from '../../components/plugins/LocateItemButton';
import {
type StockOperationProps,
- stockLocationFields,
- useCountStockItem,
- useTransferStockItem
+ stockLocationFields
} from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
import {
@@ -43,6 +40,7 @@ import {
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
+import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
import { useUserState } from '../../states/UserState';
import { PartListTable } from '../../tables/part/PartTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
@@ -259,7 +257,7 @@ export default function Stock() {
}
});
- const stockItemActionProps: StockOperationProps = useMemo(() => {
+ const stockOperationProps: StockOperationProps = useMemo(() => {
return {
pk: location.pk,
model: 'location',
@@ -270,8 +268,13 @@ export default function Stock() {
};
}, [location]);
- const transferStockItems = useTransferStockItem(stockItemActionProps);
- const countStockItems = useCountStockItem(stockItemActionProps);
+ const stockAdjustActions = useStockAdjustActions({
+ formProps: stockOperationProps,
+ enabled: true,
+ delete: false,
+ merge: false,
+ assign: false
+ });
const scanInStockItem = useBarcodeScanDialog({
title: t`Scan Stock Item`,
@@ -362,28 +365,7 @@ export default function Stock() {
enableLabels
enableReports
/>,
- }
- actions={[
- {
- name: t`Count Stock`,
- icon: (
-
- ),
- tooltip: t`Count Stock`,
- onClick: () => countStockItems.open()
- },
- {
- name: 'Transfer Stock',
- icon: (
-
- ),
- tooltip: 'Transfer Stock',
- onClick: () => transferStockItems.open()
- }
- ]}
- />,
+ stockAdjustActions.dropdown,
],
- [location, id, user]
+ [location, id, user, stockAdjustActions.dropdown]
);
const breadcrumbs = useMemo(
@@ -463,9 +445,8 @@ export default function Stock() {
id={location?.pk}
instance={location}
/>
- {transferStockItems.modal}
- {countStockItems.modal}
+ {stockAdjustActions.modals.map((modal) => modal.modal)}
>
);
diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx
index 5abc143d75..9dec661b09 100644
--- a/src/frontend/src/pages/stock/StockDetail.tsx
+++ b/src/frontend/src/pages/stock/StockDetail.tsx
@@ -68,14 +68,9 @@ import { useApi } from '../../contexts/ApiContext';
import { formatCurrency } from '../../defaults/formatters';
import {
type StockOperationProps,
- useAddStockItem,
- useAssignStockItem,
- useCountStockItem,
useFindSerialNumberForm,
- useRemoveStockItem,
useStockFields,
- useStockItemSerializeFields,
- useTransferStockItem
+ useStockItemSerializeFields
} from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
import {
@@ -84,6 +79,7 @@ import {
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
+import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
import { useGlobalSettingsState } from '../../states/SettingsStates';
import { useUserState } from '../../states/UserState';
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
@@ -715,7 +711,7 @@ export default function StockDetail() {
}
});
- const stockActionProps: StockOperationProps = useMemo(() => {
+ const stockOperationProps: StockOperationProps = useMemo(() => {
return {
items: [stockitem],
model: ModelType.stockitem,
@@ -726,11 +722,11 @@ export default function StockDetail() {
};
}, [stockitem]);
- const countStockItem = useCountStockItem(stockActionProps);
- const addStockItem = useAddStockItem(stockActionProps);
- const removeStockItem = useRemoveStockItem(stockActionProps);
- const transferStockItem = useTransferStockItem(stockActionProps);
- const assignToCustomer = useAssignStockItem(stockActionProps);
+ const stockAdjustActions = useStockAdjustActions({
+ formProps: stockOperationProps,
+ delete: false,
+ merge: false
+ });
const serializeStockFields = useStockItemSerializeFields({
partId: stockitem.part,
@@ -862,50 +858,7 @@ export default function StockDetail() {
tooltip={t`Stock Operations`}
icon={}
actions={[
- {
- name: t`Count`,
- tooltip: t`Count stock`,
- hidden: serialized || !canTransfer || isBuilding,
- icon: (
-
- ),
- onClick: () => {
- stockitem.pk && countStockItem.open();
- }
- },
- {
- name: t`Add`,
- tooltip: t`Add Stock`,
- hidden: serialized || !canTransfer || isBuilding,
- icon: ,
- onClick: () => {
- stockitem.pk && addStockItem.open();
- }
- },
- {
- name: t`Remove`,
- tooltip: t`Remove Stock`,
- hidden:
- serialized ||
- !canTransfer ||
- isBuilding ||
- stockitem.quantity <= 0,
- icon: ,
- onClick: () => {
- stockitem.pk && removeStockItem.open();
- }
- },
- {
- name: t`Transfer`,
- tooltip: t`Transfer Stock`,
- hidden: !canTransfer,
- icon: (
-
- ),
- onClick: () => {
- stockitem.pk && transferStockItem.open();
- }
- },
+ ...stockAdjustActions.menuActions,
{
name: t`Serialize`,
tooltip: t`Serialize stock`,
@@ -944,17 +897,6 @@ export default function StockDetail() {
onClick: () => {
stockitem.pk && returnStockItem.open();
}
- },
- {
- name: t`Assign to Customer`,
- tooltip: t`Assign to a customer`,
- hidden: !!stockitem.customer,
- icon: (
-
- ),
- onClick: () => {
- stockitem.pk && assignToCustomer.open();
- }
}
]}
/>,
@@ -976,7 +918,7 @@ export default function StockDetail() {
]}
/>
];
- }, [id, stockitem, user]);
+ }, [id, stockitem, user, stockAdjustActions.menuActions]);
const stockBadges: ReactNode[] = useMemo(() => {
let available = (stockitem?.quantity ?? 0) - (stockitem?.allocated ?? 0);
@@ -1099,13 +1041,9 @@ export default function StockDetail() {
{editStockItem.modal}
{duplicateStockItem.modal}
{deleteStockItem.modal}
- {countStockItem.modal}
- {addStockItem.modal}
- {removeStockItem.modal}
- {transferStockItem.modal}
{serializeStockItem.modal}
{returnStockItem.modal}
- {assignToCustomer.modal}
+ {stockAdjustActions.modals.map((modal) => modal.modal)}
{orderPartsWizard.wizard}
diff --git a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx
index 0c96daa9ca..502590fbb6 100644
--- a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx
+++ b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx
@@ -6,10 +6,12 @@ import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import type { TableFilter } from '@lib/types/Filters';
+import type { StockOperationProps } from '../../forms/StockForms';
import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
+import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import type { TableColumn } from '../Column';
@@ -178,6 +180,37 @@ export default function BuildAllocatedStockTable({
table: table
});
+ const stockOperationProps: StockOperationProps = useMemo(() => {
+ // Extract stock items from the selected records
+ // Note that the table is actually a list of BuildItem instances,
+ // so we need to reconstruct the stock item details
+ const stockItems: any[] = table.selectedRecords
+ .filter((item: any) => !!item.stock_item_detail)
+ .map((item: any) => {
+ return {
+ ...item.stock_item_detail,
+ part_detail: item.part_detail,
+ location_detail: item.location_detail
+ };
+ });
+
+ return {
+ items: stockItems,
+ model: ModelType.stockitem,
+ refresh: table.refreshTable
+ };
+ }, [table.selectedRecords, table.refreshTable]);
+
+ const stockAdjustActions = useStockAdjustActions({
+ formProps: stockOperationProps,
+ merge: false,
+ assign: false,
+ delete: false,
+ add: false,
+ count: false,
+ remove: false
+ });
+
const rowActions = useCallback(
(record: any): RowAction[] => {
return [
@@ -200,10 +233,15 @@ export default function BuildAllocatedStockTable({
[user]
);
+ const tableActions = useMemo(() => {
+ return [stockAdjustActions.dropdown];
+ }, [stockAdjustActions.dropdown]);
+
return (
<>
{editItem.modal}
{deleteItem.modal}
+ {stockAdjustActions.modals.map((modal) => modal.modal)}
{
+ return {
+ items: table.selectedRecords,
+ model: ModelType.stockitem,
+ refresh: table.refreshTable,
+ filters: {}
+ };
+ }, [table.selectedRecords, table.refreshTable]);
+
+ const stockAdjustActions = useStockAdjustActions({
+ formProps: stockOperationProps,
+ merge: false,
+ assign: false,
+ delete: false,
+ add: false,
+ count: false,
+ remove: false
+ });
+
const tableActions = useMemo(() => {
return [
+ stockAdjustActions.dropdown,
];
- }, [build, user, table.selectedRecords, table.hasSelectedRecords]);
+ }, [
+ build,
+ user,
+ table.selectedRecords,
+ table.hasSelectedRecords,
+ stockAdjustActions.dropdown
+ ]);
const rowActions = useCallback(
(record: any): RowAction[] => {
@@ -595,6 +625,7 @@ export default function BuildOutputTable({
{editBuildOutput.modal}
{deallocateBuildOutput.modal}
{cancelBuildOutputsForm.modal}
+ {stockAdjustActions.modals.map((modal) => modal.modal)}
{
+ // Extract stock items from the selected records
+ // Note that the table is actually a list of SalesOrderAllocation instances,
+ // so we need to reconstruct the stock item details
+ const stockItems: any[] = table.selectedRecords
+ .filter((item: any) => !!item.item_detail)
+ .map((item: any) => {
+ return {
+ ...item.item_detail,
+ part_detail: item.part_detail,
+ location_detail: item.location_detail
+ };
+ });
+
+ return {
+ items: stockItems,
+ model: ModelType.stockitem,
+ refresh: table.refreshTable
+ };
+ }, [table.selectedRecords, table.refreshTable]);
+
+ const stockAdjustActions = useStockAdjustActions({
+ formProps: stockOperationProps,
+ merge: false,
+ assign: false,
+ delete: false,
+ add: false,
+ count: false,
+ remove: false
+ });
+
// A subset of the selected allocations, which can be assigned to a shipment
const nonShippedAllocationIds: number[] = useMemo(() => {
// Only allow allocations which have not been shipped
@@ -273,6 +306,7 @@ export default function SalesOrderAllocationTable({
const tableActions = useMemo(() => {
return [
+ stockAdjustActions.dropdown,
}
@@ -286,13 +320,20 @@ export default function SalesOrderAllocationTable({
// TODO: Hide if order is already shipped
/>
];
- }, [allowEdit, nonShippedAllocationIds, orderId, user]);
+ }, [
+ allowEdit,
+ nonShippedAllocationIds,
+ orderId,
+ user,
+ stockAdjustActions.dropdown
+ ]);
return (
<>
{setShipment.modal}
{editAllocation.modal}
{deleteAllocation.modal}
+ {!isSubTable && stockAdjustActions.modals.map((modal) => modal.modal)}
{
+ const stockOperationProps: StockOperationProps = useMemo(() => {
return {
items: table.selectedRecords,
model: ModelType.stockitem,
@@ -510,7 +503,7 @@ export function StockItemTable({
in_stock: true
}
};
- }, [table]);
+ }, [table.selectedRecords, table.refreshTable]);
const newStockItemFields = useStockFields({
create: true,
@@ -538,117 +531,25 @@ export function StockItemTable({
parts: partsToOrder
});
- const transferStock = useTransferStockItem(tableActionParams);
- const addStock = useAddStockItem(tableActionParams);
- const removeStock = useRemoveStockItem(tableActionParams);
- const countStock = useCountStockItem(tableActionParams);
- const changeStockStatus = useChangeStockStatus(tableActionParams);
- const mergeStock = useMergeStockItem(tableActionParams);
- const assignStock = useAssignStockItem(tableActionParams);
- const deleteStock = useDeleteStockItem(tableActionParams);
+ const stockAdjustActions = useStockAdjustActions({
+ formProps: stockOperationProps
+ });
const tableActions = useMemo(() => {
- const can_delete_stock = user.hasDeleteRole(UserRoles.stock);
- const can_add_stock = user.hasAddRole(UserRoles.stock);
- const can_add_stocktake = user.hasAddRole(UserRoles.stocktake);
-
return [
- }
- disabled={table.selectedRecords.length === 0}
- actions={[
- {
- name: t`Count Stock`,
- icon: (
-
- ),
- tooltip: t`Count Stock`,
- disabled: !can_add_stocktake,
- onClick: () => {
- countStock.open();
- }
- },
- {
- name: t`Add Stock`,
- icon: ,
- tooltip: t`Add a new stock item`,
- disabled: !can_add_stock,
- onClick: () => {
- addStock.open();
- }
- },
- {
- name: t`Remove Stock`,
- icon: ,
- tooltip: t`Remove some quantity from a stock item`,
- disabled: !can_add_stock,
- onClick: () => {
- removeStock.open();
- }
- },
- {
- name: t`Transfer Stock`,
- icon: (
-
- ),
- tooltip: t`Move Stock items to new locations`,
- disabled: !can_add_stock,
- onClick: () => {
- transferStock.open();
- }
- },
- {
- name: t`Change stock status`,
- icon: ,
- tooltip: t`Change the status of stock items`,
- disabled: !can_add_stock,
- onClick: () => {
- changeStockStatus.open();
- }
- },
- {
- name: t`Merge stock`,
- icon: ,
- tooltip: t`Merge stock items`,
- disabled: !can_add_stock,
- onClick: () => {
- mergeStock.open();
- }
- },
- {
- name: t`Order stock`,
- icon: ,
- tooltip: t`Order new stock`,
- 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`,
- icon: ,
- tooltip: t`Assign items to a customer`,
- disabled: !can_add_stock,
- onClick: () => {
- assignStock.open();
- }
- },
- {
- name: t`Delete stock`,
- icon: ,
- tooltip: t`Delete Stock Items`,
- disabled: !can_delete_stock,
- onClick: () => {
- deleteStock.open();
- }
- }
- ]}
+ stockAdjustActions.dropdown,
+ }
+ disabled={!table.hasSelectedRecords}
+ onClick={() => {
+ setPartsToOrder(
+ table.selectedRecords.map((record) => record.part_detail)
+ );
+ orderPartsWizard.openWizard();
+ }}
/>,
newStockItem.open()}
/>
];
- }, [user, allowAdd, table.hasSelectedRecords, table.selectedRecords]);
+ }, [
+ user,
+ allowAdd,
+ table.hasSelectedRecords,
+ table.selectedRecords,
+ stockAdjustActions.dropdown
+ ]);
return (
<>
{newStockItem.modal}
- {transferStock.modal}
- {removeStock.modal}
- {addStock.modal}
- {countStock.modal}
- {changeStockStatus.modal}
- {mergeStock.modal}
- {assignStock.modal}
- {deleteStock.modal}
{orderPartsWizard.wizard}
+ {stockAdjustActions.modals.map((modal) => modal.modal)}
{
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('button', { name: 'action-button-order-items' }).click();
await page.getByRole('banner').getByRole('button').click();
// Order from the part detail page