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, +