mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-06 21:50:55 +00:00
[UI] Sales order button (#9938)
* Refactor stock actions into a hook * Cleanup hooks * Allow transfer of "incomplete" outputs * Simplify even further * Enable transfer of stock allocated to build order * Remove StockItemBriefSerializer entirely * stock actions for items allocated to sales order * Refactor <PartDetail> page * Refactor <StockDetail> * Refactor <LocationDetail> * Bump API version * SO button text - Observe the "SALESORDER_SHIP_COMPLETE" value * Fix palywright tests
This commit is contained in:
@ -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
|
||||
|
||||
|
@ -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')
|
||||
)
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -57,7 +57,7 @@ export function ActionDropdown({
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
noindicator?: boolean;
|
||||
}) {
|
||||
}): ReactNode {
|
||||
const hasActions = useMemo(() => {
|
||||
return actions.some((action) => !action.hidden);
|
||||
}, [actions]);
|
||||
|
@ -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`;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
208
src/frontend/src/hooks/UseStockAdjustActions.tsx
Normal file
208
src/frontend/src/hooks/UseStockAdjustActions.tsx
Normal file
@ -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: <InvenTreeIcon icon='stocktake' iconProps={{ color: 'blue' }} />,
|
||||
tooltip: t`Count selected stock items`,
|
||||
onClick: () => {
|
||||
countStock.open();
|
||||
}
|
||||
});
|
||||
|
||||
props.add != false &&
|
||||
menuActions.push({
|
||||
name: t`Add Stock`,
|
||||
icon: <InvenTreeIcon icon='add' iconProps={{ color: 'green' }} />,
|
||||
tooltip: t`Add to selected stock items`,
|
||||
onClick: () => {
|
||||
addStock.open();
|
||||
}
|
||||
});
|
||||
|
||||
props.remove != false &&
|
||||
menuActions.push({
|
||||
name: t`Remove Stock`,
|
||||
icon: <InvenTreeIcon icon='remove' iconProps={{ color: 'red' }} />,
|
||||
tooltip: t`Remove from selected stock items`,
|
||||
onClick: () => {
|
||||
removeStock.open();
|
||||
}
|
||||
});
|
||||
|
||||
props.transfer != false &&
|
||||
menuActions.push({
|
||||
name: t`Transfer Stock`,
|
||||
icon: <InvenTreeIcon icon='transfer' iconProps={{ color: 'blue' }} />,
|
||||
tooltip: t`Transfer selected stock items`,
|
||||
onClick: () => {
|
||||
transferStock.open();
|
||||
}
|
||||
});
|
||||
|
||||
props.merge != false &&
|
||||
menuActions.push({
|
||||
name: t`Merge Stock`,
|
||||
icon: <InvenTreeIcon icon='merge' />,
|
||||
tooltip: t`Merge selected stock items`,
|
||||
onClick: () => {
|
||||
mergeStock.open();
|
||||
}
|
||||
});
|
||||
|
||||
props.changeStatus != false &&
|
||||
menuActions.push({
|
||||
name: t`Change Status`,
|
||||
icon: <InvenTreeIcon icon='info' iconProps={{ color: 'blue' }} />,
|
||||
tooltip: t`Change status of selected stock items`,
|
||||
onClick: () => {
|
||||
changeStatus.open();
|
||||
}
|
||||
});
|
||||
|
||||
props.assign != false &&
|
||||
menuActions.push({
|
||||
name: t`Assign Stock`,
|
||||
icon: <InvenTreeIcon icon='customer' />,
|
||||
tooltip: t`Assign selected stock items to a customer`,
|
||||
onClick: () => {
|
||||
assignStock.open();
|
||||
}
|
||||
});
|
||||
|
||||
props.delete != false &&
|
||||
menuActions.push({
|
||||
name: t`Delete Stock`,
|
||||
icon: <InvenTreeIcon icon='delete' iconProps={{ color: 'red' }} />,
|
||||
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 (
|
||||
<ActionDropdown
|
||||
key='stock-adjust-actions'
|
||||
tooltip={t`Stock Actions`}
|
||||
icon={<InvenTreeIcon icon='stock' />}
|
||||
actions={menuActions}
|
||||
disabled={
|
||||
props.enabled != true && (props.enabled == false || !hasItems)
|
||||
}
|
||||
hidden={!hasActions}
|
||||
/>
|
||||
);
|
||||
}, [props.enabled, hasActions, hasItems, menuActions]);
|
||||
|
||||
return {
|
||||
modals,
|
||||
menuActions,
|
||||
hasActions,
|
||||
dropdown
|
||||
};
|
||||
}
|
@ -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={<IconPackages />}
|
||||
actions={[
|
||||
{
|
||||
icon: (
|
||||
<InvenTreeIcon icon='stocktake' iconProps={{ color: 'blue' }} />
|
||||
),
|
||||
name: t`Count Stock`,
|
||||
tooltip: t`Count part stock`,
|
||||
hidden: !user.hasChangeRole(UserRoles.stock),
|
||||
onClick: () => {
|
||||
part.pk && countStockItems.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<InvenTreeIcon icon='transfer' iconProps={{ color: 'blue' }} />
|
||||
),
|
||||
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)}
|
||||
<InstanceDetail query={instanceQuery} requiredRole={UserRoles.part}>
|
||||
<Stack gap='xs'>
|
||||
{user.hasViewRole(UserRoles.part_category) && (
|
||||
|
@ -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 [
|
||||
<PrimaryActionButton
|
||||
@ -537,7 +543,7 @@ export default function SalesOrderDetail() {
|
||||
]}
|
||||
/>
|
||||
];
|
||||
}, [user, order, soStatus]);
|
||||
}, [user, order, soStatus, globalSettings]);
|
||||
|
||||
const orderBadges: ReactNode[] = useMemo(() => {
|
||||
return instanceQuery.isLoading
|
||||
|
@ -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
|
||||
/>,
|
||||
<ActionDropdown
|
||||
tooltip={t`Stock Actions`}
|
||||
icon={<InvenTreeIcon icon='stock' />}
|
||||
actions={[
|
||||
{
|
||||
name: t`Count Stock`,
|
||||
icon: (
|
||||
<InvenTreeIcon icon='stocktake' iconProps={{ color: 'blue' }} />
|
||||
),
|
||||
tooltip: t`Count Stock`,
|
||||
onClick: () => countStockItems.open()
|
||||
},
|
||||
{
|
||||
name: 'Transfer Stock',
|
||||
icon: (
|
||||
<InvenTreeIcon icon='transfer' iconProps={{ color: 'blue' }} />
|
||||
),
|
||||
tooltip: 'Transfer Stock',
|
||||
onClick: () => transferStockItems.open()
|
||||
}
|
||||
]}
|
||||
/>,
|
||||
stockAdjustActions.dropdown,
|
||||
<OptionsActionDropdown
|
||||
tooltip={t`Location Actions`}
|
||||
actions={[
|
||||
@ -400,7 +382,7 @@ export default function Stock() {
|
||||
]}
|
||||
/>
|
||||
],
|
||||
[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}
|
||||
</Stack>
|
||||
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||
</InstanceDetail>
|
||||
</>
|
||||
);
|
||||
|
@ -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={<IconPackages />}
|
||||
actions={[
|
||||
{
|
||||
name: t`Count`,
|
||||
tooltip: t`Count stock`,
|
||||
hidden: serialized || !canTransfer || isBuilding,
|
||||
icon: (
|
||||
<InvenTreeIcon icon='stocktake' iconProps={{ color: 'blue' }} />
|
||||
),
|
||||
onClick: () => {
|
||||
stockitem.pk && countStockItem.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Add`,
|
||||
tooltip: t`Add Stock`,
|
||||
hidden: serialized || !canTransfer || isBuilding,
|
||||
icon: <InvenTreeIcon icon='add' iconProps={{ color: 'green' }} />,
|
||||
onClick: () => {
|
||||
stockitem.pk && addStockItem.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Remove`,
|
||||
tooltip: t`Remove Stock`,
|
||||
hidden:
|
||||
serialized ||
|
||||
!canTransfer ||
|
||||
isBuilding ||
|
||||
stockitem.quantity <= 0,
|
||||
icon: <InvenTreeIcon icon='remove' iconProps={{ color: 'red' }} />,
|
||||
onClick: () => {
|
||||
stockitem.pk && removeStockItem.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Transfer`,
|
||||
tooltip: t`Transfer Stock`,
|
||||
hidden: !canTransfer,
|
||||
icon: (
|
||||
<InvenTreeIcon icon='transfer' iconProps={{ color: 'blue' }} />
|
||||
),
|
||||
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: (
|
||||
<InvenTreeIcon icon='customer' iconProps={{ color: 'blue' }} />
|
||||
),
|
||||
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}
|
||||
</Stack>
|
||||
</InstanceDetail>
|
||||
|
@ -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)}
|
||||
<InvenTreeTable
|
||||
tableState={table}
|
||||
url={apiUrl(ApiEndpoints.build_item_list)}
|
||||
@ -223,6 +261,7 @@ export default function BuildAllocatedStockTable({
|
||||
enableDownload: true,
|
||||
enableSelection: allowEdit && user.hasDeleteRole(UserRoles.build),
|
||||
rowActions: rowActions,
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters,
|
||||
modelField: modelField ?? 'stock_item',
|
||||
modelType: modelTarget ?? ModelType.stockitem
|
||||
|
@ -36,12 +36,16 @@ import {
|
||||
useCompleteBuildOutputsForm,
|
||||
useScrapBuildOutputsForm
|
||||
} from '../../forms/BuildForms';
|
||||
import { useStockFields } from '../../forms/StockForms';
|
||||
import {
|
||||
type StockOperationProps,
|
||||
useStockFields
|
||||
} from '../../forms/StockForms';
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import type { TableColumn } from '../Column';
|
||||
@ -368,8 +372,28 @@ export default function BuildOutputTable({
|
||||
];
|
||||
}, []);
|
||||
|
||||
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||
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,
|
||||
<ActionButton
|
||||
key='complete-selected-outputs'
|
||||
tooltip={t`Complete selected outputs`}
|
||||
@ -410,7 +434,13 @@ export default function BuildOutputTable({
|
||||
onClick={addBuildOutput.open}
|
||||
/>
|
||||
];
|
||||
}, [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)}
|
||||
<OutputAllocationDrawer
|
||||
build={build}
|
||||
output={selectedOutputs[0]}
|
||||
|
@ -10,11 +10,13 @@ import { IconTruckDelivery } from '@tabler/icons-react';
|
||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||
import { formatDate } from '../../defaults/formatters';
|
||||
import { useSalesOrderAllocationFields } from '../../forms/SalesOrderForms';
|
||||
import type { StockOperationProps } from '../../forms/StockForms';
|
||||
import {
|
||||
useBulkEditApiFormModal,
|
||||
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';
|
||||
@ -246,6 +248,37 @@ export default function SalesOrderAllocationTable({
|
||||
[allowEdit, user]
|
||||
);
|
||||
|
||||
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||
// 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,
|
||||
<ActionButton
|
||||
tooltip={t`Assign to shipment`}
|
||||
icon={<IconTruckDelivery />}
|
||||
@ -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)}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.sales_order_allocation_list)}
|
||||
tableState={table}
|
||||
|
@ -7,24 +7,17 @@ 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 { ActionButton } from '../../components/buttons/ActionButton';
|
||||
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 {
|
||||
type StockOperationProps,
|
||||
useAddStockItem,
|
||||
useAssignStockItem,
|
||||
useChangeStockStatus,
|
||||
useCountStockItem,
|
||||
useDeleteStockItem,
|
||||
useMergeStockItem,
|
||||
useRemoveStockItem,
|
||||
useStockFields,
|
||||
useTransferStockItem
|
||||
useStockFields
|
||||
} from '../../forms/StockForms';
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
@ -501,7 +494,7 @@ export function StockItemTable({
|
||||
[stockExpiryEnabled]
|
||||
);
|
||||
|
||||
const tableActionParams: StockOperationProps = useMemo(() => {
|
||||
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 [
|
||||
<ActionDropdown
|
||||
key='stock-actions'
|
||||
tooltip={t`Stock Actions`}
|
||||
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' }} />,
|
||||
tooltip: t`Add a new stock item`,
|
||||
disabled: !can_add_stock,
|
||||
onClick: () => {
|
||||
addStock.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Remove Stock`,
|
||||
icon: <InvenTreeIcon icon='remove' iconProps={{ color: 'red' }} />,
|
||||
tooltip: t`Remove some quantity from a stock item`,
|
||||
disabled: !can_add_stock,
|
||||
onClick: () => {
|
||||
removeStock.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Transfer Stock`,
|
||||
icon: (
|
||||
<InvenTreeIcon icon='transfer' iconProps={{ color: 'blue' }} />
|
||||
),
|
||||
tooltip: t`Move Stock items to new locations`,
|
||||
disabled: !can_add_stock,
|
||||
onClick: () => {
|
||||
transferStock.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Change stock status`,
|
||||
icon: <InvenTreeIcon icon='info' iconProps={{ color: 'blue' }} />,
|
||||
tooltip: t`Change the status of stock items`,
|
||||
disabled: !can_add_stock,
|
||||
onClick: () => {
|
||||
changeStockStatus.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Merge stock`,
|
||||
icon: <InvenTreeIcon icon='merge' />,
|
||||
tooltip: t`Merge stock items`,
|
||||
disabled: !can_add_stock,
|
||||
onClick: () => {
|
||||
mergeStock.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Order stock`,
|
||||
icon: <InvenTreeIcon icon='buy' />,
|
||||
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: <InvenTreeIcon icon='customer' />,
|
||||
tooltip: t`Assign items to a customer`,
|
||||
disabled: !can_add_stock,
|
||||
onClick: () => {
|
||||
assignStock.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Delete stock`,
|
||||
icon: <InvenTreeIcon icon='delete' iconProps={{ color: 'red' }} />,
|
||||
tooltip: t`Delete Stock Items`,
|
||||
disabled: !can_delete_stock,
|
||||
onClick: () => {
|
||||
deleteStock.open();
|
||||
}
|
||||
}
|
||||
]}
|
||||
stockAdjustActions.dropdown,
|
||||
<ActionButton
|
||||
key='stock-order'
|
||||
hidden={!user.hasAddRole(UserRoles.purchase_order)}
|
||||
tooltip={t`Order items`}
|
||||
icon={<InvenTreeIcon icon='buy' />}
|
||||
disabled={!table.hasSelectedRecords}
|
||||
onClick={() => {
|
||||
setPartsToOrder(
|
||||
table.selectedRecords.map((record) => record.part_detail)
|
||||
);
|
||||
orderPartsWizard.openWizard();
|
||||
}}
|
||||
/>,
|
||||
<AddItemButton
|
||||
key='add-stock-item'
|
||||
@ -657,20 +558,19 @@ export function StockItemTable({
|
||||
onClick={() => 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)}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||
tableState={table}
|
||||
|
@ -249,11 +249,7 @@ test('Purchase Orders - Order Parts', async ({ browser }) => {
|
||||
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
|
||||
|
Reference in New Issue
Block a user