mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-07 06:00:57 +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 information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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
|
v359 -> 2025-07-01 : https://github.com/inventree/InvenTree/pull/9909
|
||||||
- Fixes annotated types for various part fields
|
- 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.models import StockItem, StockLocation
|
||||||
from stock.serializers import (
|
from stock.serializers import (
|
||||||
LocationBriefSerializer,
|
LocationBriefSerializer,
|
||||||
StockItemSerializerBrief,
|
StockItemSerializer,
|
||||||
StockStatusCustomSerializer,
|
StockStatusCustomSerializer,
|
||||||
)
|
)
|
||||||
from stock.status_codes import StockStatus
|
from stock.status_codes import StockStatus
|
||||||
@ -1227,7 +1227,7 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
pricing=False,
|
pricing=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
stock_item_detail = StockItemSerializerBrief(
|
stock_item_detail = StockItemSerializer(
|
||||||
source='stock_item', read_only=True, allow_null=True, label=_('Stock Item')
|
source='stock_item', read_only=True, allow_null=True, label=_('Stock Item')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1430,7 +1430,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
|||||||
part_detail = PartBriefSerializer(
|
part_detail = PartBriefSerializer(
|
||||||
source='item.part', many=False, read_only=True, allow_null=True
|
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
|
source='item', many=False, read_only=True, allow_null=True
|
||||||
)
|
)
|
||||||
location_detail = stock.serializers.LocationBriefSerializer(
|
location_detail = stock.serializers.LocationBriefSerializer(
|
||||||
|
@ -307,41 +307,6 @@ class StockItemTestResultSerializer(
|
|||||||
return data
|
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()
|
@register_importer()
|
||||||
class StockItemSerializer(
|
class StockItemSerializer(
|
||||||
DataImportExportSerializerMixin,
|
DataImportExportSerializerMixin,
|
||||||
@ -1336,7 +1301,7 @@ class StockTrackingSerializer(
|
|||||||
|
|
||||||
label = serializers.CharField(read_only=True)
|
label = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
item_detail = StockItemSerializerBrief(
|
item_detail = StockItemSerializer(
|
||||||
source='item', many=False, read_only=True, allow_null=True
|
source='item', many=False, read_only=True, allow_null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ export function ActionDropdown({
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
noindicator?: boolean;
|
noindicator?: boolean;
|
||||||
}) {
|
}): ReactNode {
|
||||||
const hasActions = useMemo(() => {
|
const hasActions = useMemo(() => {
|
||||||
return actions.some((action) => !action.hidden);
|
return actions.some((action) => !action.hidden);
|
||||||
}, [actions]);
|
}, [actions]);
|
||||||
|
@ -436,7 +436,10 @@ function LineItemFormRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Selected location is base part's default location
|
// 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`;
|
return t`Default location selected`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -934,7 +934,7 @@ function stockMergeFields(items: any[]): ApiFormFieldSet {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
default: items[0]?.part_detail.default_location,
|
default: items[0]?.part_detail?.default_location,
|
||||||
filters: {
|
filters: {
|
||||||
structural: false
|
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 { usePartFields } from '../../forms/PartForms';
|
||||||
import {
|
import {
|
||||||
type StockOperationProps,
|
type StockOperationProps,
|
||||||
useCountStockItem,
|
useFindSerialNumberForm
|
||||||
useFindSerialNumberForm,
|
|
||||||
useTransferStockItem
|
|
||||||
} from '../../forms/StockForms';
|
} from '../../forms/StockForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
|
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
|
||||||
import {
|
import {
|
||||||
useGlobalSettingsState,
|
useGlobalSettingsState,
|
||||||
useUserSettingsState
|
useUserSettingsState
|
||||||
@ -899,7 +897,7 @@ export default function PartDetail() {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const stockActionProps: StockOperationProps = useMemo(() => {
|
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
pk: part.pk,
|
pk: part.pk,
|
||||||
model: ModelType.part,
|
model: ModelType.part,
|
||||||
@ -910,8 +908,11 @@ export default function PartDetail() {
|
|||||||
};
|
};
|
||||||
}, [part]);
|
}, [part]);
|
||||||
|
|
||||||
const countStockItems = useCountStockItem(stockActionProps);
|
const stockAdjustActions = useStockAdjustActions({
|
||||||
const transferStockItems = useTransferStockItem(stockActionProps);
|
formProps: stockOperationProps,
|
||||||
|
merge: false,
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
|
||||||
const orderPartsWizard = OrderPartsWizard({
|
const orderPartsWizard = OrderPartsWizard({
|
||||||
parts: [part]
|
parts: [part]
|
||||||
@ -947,28 +948,7 @@ export default function PartDetail() {
|
|||||||
tooltip={t`Stock Actions`}
|
tooltip={t`Stock Actions`}
|
||||||
icon={<IconPackages />}
|
icon={<IconPackages />}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
...stockAdjustActions.menuActions,
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: t`Order`,
|
name: t`Order`,
|
||||||
tooltip: t`Order Stock`,
|
tooltip: t`Order Stock`,
|
||||||
@ -1009,7 +989,7 @@ export default function PartDetail() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [id, part, user]);
|
}, [id, part, user, stockAdjustActions.menuActions]);
|
||||||
|
|
||||||
const enableRevisionSelection: boolean = useMemo(() => {
|
const enableRevisionSelection: boolean = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@ -1023,10 +1003,9 @@ export default function PartDetail() {
|
|||||||
{editPart.modal}
|
{editPart.modal}
|
||||||
{deletePart.modal}
|
{deletePart.modal}
|
||||||
{duplicatePart.modal}
|
{duplicatePart.modal}
|
||||||
{countStockItems.modal}
|
|
||||||
{orderPartsWizard.wizard}
|
{orderPartsWizard.wizard}
|
||||||
{findBySerialNumber.modal}
|
{findBySerialNumber.modal}
|
||||||
{transferStockItems.modal}
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
<InstanceDetail query={instanceQuery} requiredRole={UserRoles.part}>
|
<InstanceDetail query={instanceQuery} requiredRole={UserRoles.part}>
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
{user.hasViewRole(UserRoles.part_category) && (
|
{user.hasViewRole(UserRoles.part_category) && (
|
||||||
|
@ -475,8 +475,14 @@ export default function SalesOrderDetail() {
|
|||||||
(order.status == soStatus.PENDING ||
|
(order.status == soStatus.PENDING ||
|
||||||
order.status == soStatus.IN_PROGRESS);
|
order.status == soStatus.IN_PROGRESS);
|
||||||
|
|
||||||
const canShip: boolean = canEdit && order.status == soStatus.IN_PROGRESS;
|
const autoComplete = globalSettings.isSet('SALESORDER_SHIP_COMPLETE');
|
||||||
const canComplete: boolean = canEdit && order.status == soStatus.SHIPPED;
|
|
||||||
|
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 [
|
return [
|
||||||
<PrimaryActionButton
|
<PrimaryActionButton
|
||||||
@ -537,7 +543,7 @@ export default function SalesOrderDetail() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [user, order, soStatus]);
|
}, [user, order, soStatus, globalSettings]);
|
||||||
|
|
||||||
const orderBadges: ReactNode[] = useMemo(() => {
|
const orderBadges: ReactNode[] = useMemo(() => {
|
||||||
return instanceQuery.isLoading
|
return instanceQuery.isLoading
|
||||||
|
@ -18,7 +18,6 @@ import {
|
|||||||
} from '../../components/details/Details';
|
} from '../../components/details/Details';
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
EditItemAction,
|
EditItemAction,
|
||||||
@ -33,9 +32,7 @@ import { PanelGroup } from '../../components/panels/PanelGroup';
|
|||||||
import LocateItemButton from '../../components/plugins/LocateItemButton';
|
import LocateItemButton from '../../components/plugins/LocateItemButton';
|
||||||
import {
|
import {
|
||||||
type StockOperationProps,
|
type StockOperationProps,
|
||||||
stockLocationFields,
|
stockLocationFields
|
||||||
useCountStockItem,
|
|
||||||
useTransferStockItem
|
|
||||||
} from '../../forms/StockForms';
|
} from '../../forms/StockForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import {
|
import {
|
||||||
@ -43,6 +40,7 @@ import {
|
|||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
|
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { PartListTable } from '../../tables/part/PartTable';
|
import { PartListTable } from '../../tables/part/PartTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
@ -259,7 +257,7 @@ export default function Stock() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const stockItemActionProps: StockOperationProps = useMemo(() => {
|
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
pk: location.pk,
|
pk: location.pk,
|
||||||
model: 'location',
|
model: 'location',
|
||||||
@ -270,8 +268,13 @@ export default function Stock() {
|
|||||||
};
|
};
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
const transferStockItems = useTransferStockItem(stockItemActionProps);
|
const stockAdjustActions = useStockAdjustActions({
|
||||||
const countStockItems = useCountStockItem(stockItemActionProps);
|
formProps: stockOperationProps,
|
||||||
|
enabled: true,
|
||||||
|
delete: false,
|
||||||
|
merge: false,
|
||||||
|
assign: false
|
||||||
|
});
|
||||||
|
|
||||||
const scanInStockItem = useBarcodeScanDialog({
|
const scanInStockItem = useBarcodeScanDialog({
|
||||||
title: t`Scan Stock Item`,
|
title: t`Scan Stock Item`,
|
||||||
@ -362,28 +365,7 @@ export default function Stock() {
|
|||||||
enableLabels
|
enableLabels
|
||||||
enableReports
|
enableReports
|
||||||
/>,
|
/>,
|
||||||
<ActionDropdown
|
stockAdjustActions.dropdown,
|
||||||
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()
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>,
|
|
||||||
<OptionsActionDropdown
|
<OptionsActionDropdown
|
||||||
tooltip={t`Location Actions`}
|
tooltip={t`Location Actions`}
|
||||||
actions={[
|
actions={[
|
||||||
@ -400,7 +382,7 @@ export default function Stock() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
],
|
],
|
||||||
[location, id, user]
|
[location, id, user, stockAdjustActions.dropdown]
|
||||||
);
|
);
|
||||||
|
|
||||||
const breadcrumbs = useMemo(
|
const breadcrumbs = useMemo(
|
||||||
@ -463,9 +445,8 @@ export default function Stock() {
|
|||||||
id={location?.pk}
|
id={location?.pk}
|
||||||
instance={location}
|
instance={location}
|
||||||
/>
|
/>
|
||||||
{transferStockItems.modal}
|
|
||||||
{countStockItems.modal}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
</InstanceDetail>
|
</InstanceDetail>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -68,14 +68,9 @@ import { useApi } from '../../contexts/ApiContext';
|
|||||||
import { formatCurrency } from '../../defaults/formatters';
|
import { formatCurrency } from '../../defaults/formatters';
|
||||||
import {
|
import {
|
||||||
type StockOperationProps,
|
type StockOperationProps,
|
||||||
useAddStockItem,
|
|
||||||
useAssignStockItem,
|
|
||||||
useCountStockItem,
|
|
||||||
useFindSerialNumberForm,
|
useFindSerialNumberForm,
|
||||||
useRemoveStockItem,
|
|
||||||
useStockFields,
|
useStockFields,
|
||||||
useStockItemSerializeFields,
|
useStockItemSerializeFields
|
||||||
useTransferStockItem
|
|
||||||
} from '../../forms/StockForms';
|
} from '../../forms/StockForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import {
|
import {
|
||||||
@ -84,6 +79,7 @@ import {
|
|||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
|
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
|
||||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
||||||
@ -715,7 +711,7 @@ export default function StockDetail() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const stockActionProps: StockOperationProps = useMemo(() => {
|
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
items: [stockitem],
|
items: [stockitem],
|
||||||
model: ModelType.stockitem,
|
model: ModelType.stockitem,
|
||||||
@ -726,11 +722,11 @@ export default function StockDetail() {
|
|||||||
};
|
};
|
||||||
}, [stockitem]);
|
}, [stockitem]);
|
||||||
|
|
||||||
const countStockItem = useCountStockItem(stockActionProps);
|
const stockAdjustActions = useStockAdjustActions({
|
||||||
const addStockItem = useAddStockItem(stockActionProps);
|
formProps: stockOperationProps,
|
||||||
const removeStockItem = useRemoveStockItem(stockActionProps);
|
delete: false,
|
||||||
const transferStockItem = useTransferStockItem(stockActionProps);
|
merge: false
|
||||||
const assignToCustomer = useAssignStockItem(stockActionProps);
|
});
|
||||||
|
|
||||||
const serializeStockFields = useStockItemSerializeFields({
|
const serializeStockFields = useStockItemSerializeFields({
|
||||||
partId: stockitem.part,
|
partId: stockitem.part,
|
||||||
@ -862,50 +858,7 @@ export default function StockDetail() {
|
|||||||
tooltip={t`Stock Operations`}
|
tooltip={t`Stock Operations`}
|
||||||
icon={<IconPackages />}
|
icon={<IconPackages />}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
...stockAdjustActions.menuActions,
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: t`Serialize`,
|
name: t`Serialize`,
|
||||||
tooltip: t`Serialize stock`,
|
tooltip: t`Serialize stock`,
|
||||||
@ -944,17 +897,6 @@ export default function StockDetail() {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
stockitem.pk && returnStockItem.open();
|
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(() => {
|
const stockBadges: ReactNode[] = useMemo(() => {
|
||||||
let available = (stockitem?.quantity ?? 0) - (stockitem?.allocated ?? 0);
|
let available = (stockitem?.quantity ?? 0) - (stockitem?.allocated ?? 0);
|
||||||
@ -1099,13 +1041,9 @@ export default function StockDetail() {
|
|||||||
{editStockItem.modal}
|
{editStockItem.modal}
|
||||||
{duplicateStockItem.modal}
|
{duplicateStockItem.modal}
|
||||||
{deleteStockItem.modal}
|
{deleteStockItem.modal}
|
||||||
{countStockItem.modal}
|
|
||||||
{addStockItem.modal}
|
|
||||||
{removeStockItem.modal}
|
|
||||||
{transferStockItem.modal}
|
|
||||||
{serializeStockItem.modal}
|
{serializeStockItem.modal}
|
||||||
{returnStockItem.modal}
|
{returnStockItem.modal}
|
||||||
{assignToCustomer.modal}
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
{orderPartsWizard.wizard}
|
{orderPartsWizard.wizard}
|
||||||
</Stack>
|
</Stack>
|
||||||
</InstanceDetail>
|
</InstanceDetail>
|
||||||
|
@ -6,10 +6,12 @@ import { ModelType } from '@lib/enums/ModelType';
|
|||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import type { TableFilter } from '@lib/types/Filters';
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
|
import type { StockOperationProps } from '../../forms/StockForms';
|
||||||
import {
|
import {
|
||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
|
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import type { TableColumn } from '../Column';
|
import type { TableColumn } from '../Column';
|
||||||
@ -178,6 +180,37 @@ export default function BuildAllocatedStockTable({
|
|||||||
table: table
|
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(
|
const rowActions = useCallback(
|
||||||
(record: any): RowAction[] => {
|
(record: any): RowAction[] => {
|
||||||
return [
|
return [
|
||||||
@ -200,10 +233,15 @@ export default function BuildAllocatedStockTable({
|
|||||||
[user]
|
[user]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tableActions = useMemo(() => {
|
||||||
|
return [stockAdjustActions.dropdown];
|
||||||
|
}, [stockAdjustActions.dropdown]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{editItem.modal}
|
{editItem.modal}
|
||||||
{deleteItem.modal}
|
{deleteItem.modal}
|
||||||
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
tableState={table}
|
tableState={table}
|
||||||
url={apiUrl(ApiEndpoints.build_item_list)}
|
url={apiUrl(ApiEndpoints.build_item_list)}
|
||||||
@ -223,6 +261,7 @@ export default function BuildAllocatedStockTable({
|
|||||||
enableDownload: true,
|
enableDownload: true,
|
||||||
enableSelection: allowEdit && user.hasDeleteRole(UserRoles.build),
|
enableSelection: allowEdit && user.hasDeleteRole(UserRoles.build),
|
||||||
rowActions: rowActions,
|
rowActions: rowActions,
|
||||||
|
tableActions: tableActions,
|
||||||
tableFilters: tableFilters,
|
tableFilters: tableFilters,
|
||||||
modelField: modelField ?? 'stock_item',
|
modelField: modelField ?? 'stock_item',
|
||||||
modelType: modelTarget ?? ModelType.stockitem
|
modelType: modelTarget ?? ModelType.stockitem
|
||||||
|
@ -36,12 +36,16 @@ import {
|
|||||||
useCompleteBuildOutputsForm,
|
useCompleteBuildOutputsForm,
|
||||||
useScrapBuildOutputsForm
|
useScrapBuildOutputsForm
|
||||||
} from '../../forms/BuildForms';
|
} from '../../forms/BuildForms';
|
||||||
import { useStockFields } from '../../forms/StockForms';
|
import {
|
||||||
|
type StockOperationProps,
|
||||||
|
useStockFields
|
||||||
|
} from '../../forms/StockForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
|
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import type { TableColumn } from '../Column';
|
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(() => {
|
const tableActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
stockAdjustActions.dropdown,
|
||||||
<ActionButton
|
<ActionButton
|
||||||
key='complete-selected-outputs'
|
key='complete-selected-outputs'
|
||||||
tooltip={t`Complete selected outputs`}
|
tooltip={t`Complete selected outputs`}
|
||||||
@ -410,7 +434,13 @@ export default function BuildOutputTable({
|
|||||||
onClick={addBuildOutput.open}
|
onClick={addBuildOutput.open}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [build, user, table.selectedRecords, table.hasSelectedRecords]);
|
}, [
|
||||||
|
build,
|
||||||
|
user,
|
||||||
|
table.selectedRecords,
|
||||||
|
table.hasSelectedRecords,
|
||||||
|
stockAdjustActions.dropdown
|
||||||
|
]);
|
||||||
|
|
||||||
const rowActions = useCallback(
|
const rowActions = useCallback(
|
||||||
(record: any): RowAction[] => {
|
(record: any): RowAction[] => {
|
||||||
@ -595,6 +625,7 @@ export default function BuildOutputTable({
|
|||||||
{editBuildOutput.modal}
|
{editBuildOutput.modal}
|
||||||
{deallocateBuildOutput.modal}
|
{deallocateBuildOutput.modal}
|
||||||
{cancelBuildOutputsForm.modal}
|
{cancelBuildOutputsForm.modal}
|
||||||
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
<OutputAllocationDrawer
|
<OutputAllocationDrawer
|
||||||
build={build}
|
build={build}
|
||||||
output={selectedOutputs[0]}
|
output={selectedOutputs[0]}
|
||||||
|
@ -10,11 +10,13 @@ import { IconTruckDelivery } from '@tabler/icons-react';
|
|||||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||||
import { formatDate } from '../../defaults/formatters';
|
import { formatDate } from '../../defaults/formatters';
|
||||||
import { useSalesOrderAllocationFields } from '../../forms/SalesOrderForms';
|
import { useSalesOrderAllocationFields } from '../../forms/SalesOrderForms';
|
||||||
|
import type { StockOperationProps } from '../../forms/StockForms';
|
||||||
import {
|
import {
|
||||||
useBulkEditApiFormModal,
|
useBulkEditApiFormModal,
|
||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
|
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import type { TableColumn } from '../Column';
|
import type { TableColumn } from '../Column';
|
||||||
@ -246,6 +248,37 @@ export default function SalesOrderAllocationTable({
|
|||||||
[allowEdit, user]
|
[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
|
// A subset of the selected allocations, which can be assigned to a shipment
|
||||||
const nonShippedAllocationIds: number[] = useMemo(() => {
|
const nonShippedAllocationIds: number[] = useMemo(() => {
|
||||||
// Only allow allocations which have not been shipped
|
// Only allow allocations which have not been shipped
|
||||||
@ -273,6 +306,7 @@ export default function SalesOrderAllocationTable({
|
|||||||
|
|
||||||
const tableActions = useMemo(() => {
|
const tableActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
stockAdjustActions.dropdown,
|
||||||
<ActionButton
|
<ActionButton
|
||||||
tooltip={t`Assign to shipment`}
|
tooltip={t`Assign to shipment`}
|
||||||
icon={<IconTruckDelivery />}
|
icon={<IconTruckDelivery />}
|
||||||
@ -286,13 +320,20 @@ export default function SalesOrderAllocationTable({
|
|||||||
// TODO: Hide if order is already shipped
|
// TODO: Hide if order is already shipped
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [allowEdit, nonShippedAllocationIds, orderId, user]);
|
}, [
|
||||||
|
allowEdit,
|
||||||
|
nonShippedAllocationIds,
|
||||||
|
orderId,
|
||||||
|
user,
|
||||||
|
stockAdjustActions.dropdown
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{setShipment.modal}
|
{setShipment.modal}
|
||||||
{editAllocation.modal}
|
{editAllocation.modal}
|
||||||
{deleteAllocation.modal}
|
{deleteAllocation.modal}
|
||||||
|
{!isSubTable && stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.sales_order_allocation_list)}
|
url={apiUrl(ApiEndpoints.sales_order_allocation_list)}
|
||||||
tableState={table}
|
tableState={table}
|
||||||
|
@ -7,24 +7,17 @@ import { ModelType } from '@lib/enums/ModelType';
|
|||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import type { TableFilter } from '@lib/types/Filters';
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
|
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
|
||||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||||
import { formatCurrency, formatPriceRange } from '../../defaults/formatters';
|
import { formatCurrency, formatPriceRange } from '../../defaults/formatters';
|
||||||
import {
|
import {
|
||||||
type StockOperationProps,
|
type StockOperationProps,
|
||||||
useAddStockItem,
|
useStockFields
|
||||||
useAssignStockItem,
|
|
||||||
useChangeStockStatus,
|
|
||||||
useCountStockItem,
|
|
||||||
useDeleteStockItem,
|
|
||||||
useMergeStockItem,
|
|
||||||
useRemoveStockItem,
|
|
||||||
useStockFields,
|
|
||||||
useTransferStockItem
|
|
||||||
} from '../../forms/StockForms';
|
} from '../../forms/StockForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||||
|
import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
@ -501,7 +494,7 @@ export function StockItemTable({
|
|||||||
[stockExpiryEnabled]
|
[stockExpiryEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableActionParams: StockOperationProps = useMemo(() => {
|
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
items: table.selectedRecords,
|
items: table.selectedRecords,
|
||||||
model: ModelType.stockitem,
|
model: ModelType.stockitem,
|
||||||
@ -510,7 +503,7 @@ export function StockItemTable({
|
|||||||
in_stock: true
|
in_stock: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [table]);
|
}, [table.selectedRecords, table.refreshTable]);
|
||||||
|
|
||||||
const newStockItemFields = useStockFields({
|
const newStockItemFields = useStockFields({
|
||||||
create: true,
|
create: true,
|
||||||
@ -538,117 +531,25 @@ export function StockItemTable({
|
|||||||
parts: partsToOrder
|
parts: partsToOrder
|
||||||
});
|
});
|
||||||
|
|
||||||
const transferStock = useTransferStockItem(tableActionParams);
|
const stockAdjustActions = useStockAdjustActions({
|
||||||
const addStock = useAddStockItem(tableActionParams);
|
formProps: stockOperationProps
|
||||||
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 tableActions = useMemo(() => {
|
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 [
|
return [
|
||||||
<ActionDropdown
|
stockAdjustActions.dropdown,
|
||||||
key='stock-actions'
|
<ActionButton
|
||||||
tooltip={t`Stock Actions`}
|
key='stock-order'
|
||||||
icon={<InvenTreeIcon icon='stock' />}
|
hidden={!user.hasAddRole(UserRoles.purchase_order)}
|
||||||
disabled={table.selectedRecords.length === 0}
|
tooltip={t`Order items`}
|
||||||
actions={[
|
icon={<InvenTreeIcon icon='buy' />}
|
||||||
{
|
disabled={!table.hasSelectedRecords}
|
||||||
name: t`Count Stock`,
|
onClick={() => {
|
||||||
icon: (
|
setPartsToOrder(
|
||||||
<InvenTreeIcon icon='stocktake' iconProps={{ color: 'blue' }} />
|
table.selectedRecords.map((record) => record.part_detail)
|
||||||
),
|
);
|
||||||
tooltip: t`Count Stock`,
|
orderPartsWizard.openWizard();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>,
|
/>,
|
||||||
<AddItemButton
|
<AddItemButton
|
||||||
key='add-stock-item'
|
key='add-stock-item'
|
||||||
@ -657,20 +558,19 @@ export function StockItemTable({
|
|||||||
onClick={() => newStockItem.open()}
|
onClick={() => newStockItem.open()}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [user, allowAdd, table.hasSelectedRecords, table.selectedRecords]);
|
}, [
|
||||||
|
user,
|
||||||
|
allowAdd,
|
||||||
|
table.hasSelectedRecords,
|
||||||
|
table.selectedRecords,
|
||||||
|
stockAdjustActions.dropdown
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{newStockItem.modal}
|
{newStockItem.modal}
|
||||||
{transferStock.modal}
|
|
||||||
{removeStock.modal}
|
|
||||||
{addStock.modal}
|
|
||||||
{countStock.modal}
|
|
||||||
{changeStockStatus.modal}
|
|
||||||
{mergeStock.modal}
|
|
||||||
{assignStock.modal}
|
|
||||||
{deleteStock.modal}
|
|
||||||
{orderPartsWizard.wizard}
|
{orderPartsWizard.wizard}
|
||||||
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||||
tableState={table}
|
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(`Select record ${ii}`, { exact: true }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page
|
await page.getByRole('button', { name: 'action-button-order-items' }).click();
|
||||||
.getByLabel('Stock Items')
|
|
||||||
.getByLabel('action-menu-stock-actions')
|
|
||||||
.click();
|
|
||||||
await page.getByLabel('action-menu-stock-actions-order-stock').click();
|
|
||||||
await page.getByRole('banner').getByRole('button').click();
|
await page.getByRole('banner').getByRole('button').click();
|
||||||
|
|
||||||
// Order from the part detail page
|
// Order from the part detail page
|
||||||
|
Reference in New Issue
Block a user