diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py
index 2698579273..2060c7b4b0 100644
--- a/src/backend/InvenTree/order/serializers.py
+++ b/src/backend/InvenTree/order/serializers.py
@@ -1517,37 +1517,45 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
except DjangoValidationError as e:
raise ValidationError({'serial_numbers': e.messages})
- serials_not_exist = []
- serials_allocated = []
+ serials_not_exist = set()
+ serials_unavailable = set()
stock_items_to_allocate = []
for serial in data['serials']:
+ serial = str(serial).strip()
+
items = stock.models.StockItem.objects.filter(
part=part, serial=serial, quantity=1
)
if not items.exists():
- serials_not_exist.append(str(serial))
+ serials_not_exist.add(str(serial))
continue
stock_item = items[0]
- if stock_item.unallocated_quantity() == 1:
- stock_items_to_allocate.append(stock_item)
- else:
- serials_allocated.append(str(serial))
+ if not stock_item.in_stock:
+ serials_unavailable.add(str(serial))
+ continue
+
+ if stock_item.unallocated_quantity() < 1:
+ serials_unavailable.add(str(serial))
+ continue
+
+ # At this point, the serial number is valid, and can be added to the list
+ stock_items_to_allocate.append(stock_item)
if len(serials_not_exist) > 0:
error_msg = _('No match found for the following serial numbers')
error_msg += ': '
- error_msg += ','.join(serials_not_exist)
+ error_msg += ','.join(sorted(serials_not_exist))
raise ValidationError({'serial_numbers': error_msg})
- if len(serials_allocated) > 0:
- error_msg = _('The following serial numbers are already allocated')
+ if len(serials_unavailable) > 0:
+ error_msg = _('The following serial numbers are unavailable')
error_msg += ': '
- error_msg += ','.join(serials_allocated)
+ error_msg += ','.join(sorted(serials_unavailable))
raise ValidationError({'serial_numbers': error_msg})
diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx
index 6ce43aa910..17a981b978 100644
--- a/src/frontend/src/enums/ApiEndpoints.tsx
+++ b/src/frontend/src/enums/ApiEndpoints.tsx
@@ -152,6 +152,7 @@ export enum ApiEndpoints {
sales_order_extra_line_list = 'order/so-extra-line/',
sales_order_allocation_list = 'order/so-allocation/',
sales_order_shipment_list = 'order/so/shipment/',
+ sales_order_allocate_serials = 'order/so/:id/allocate-serials/',
return_order_list = 'order/ro/',
return_order_issue = 'order/ro/:id/issue/',
diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx
index 02f5a976d5..d67ede312b 100644
--- a/src/frontend/src/forms/SalesOrderForms.tsx
+++ b/src/frontend/src/forms/SalesOrderForms.tsx
@@ -84,6 +84,31 @@ export function useSalesOrderLineItemFields({
return fields;
}
+export function useSalesOrderAllocateSerialsFields({
+ itemId,
+ orderId
+}: {
+ itemId: number;
+ orderId: number;
+}): ApiFormFieldSet {
+ return useMemo(() => {
+ return {
+ line_item: {
+ value: itemId,
+ hidden: true
+ },
+ quantity: {},
+ serial_numbers: {},
+ shipment: {
+ filters: {
+ order: orderId,
+ shipped: false
+ }
+ }
+ };
+ }, [itemId, orderId]);
+}
+
export function useSalesOrderShipmentFields(): ApiFormFieldSet {
return useMemo(() => {
return {
diff --git a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
index 240e3a5ccf..ea2e91358a 100644
--- a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
+++ b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
@@ -1,6 +1,7 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import {
+ IconHash,
IconShoppingCart,
IconSquareArrowRight,
IconTools
@@ -14,7 +15,10 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useBuildOrderFields } from '../../forms/BuildForms';
-import { useSalesOrderLineItemFields } from '../../forms/SalesOrderForms';
+import {
+ useSalesOrderAllocateSerialsFields,
+ useSalesOrderLineItemFields
+} from '../../forms/SalesOrderForms';
import { notYetImplemented } from '../../functions/notifications';
import {
useCreateApiFormModal,
@@ -223,6 +227,19 @@ export default function SalesOrderLineItemTable({
table: table
});
+ const allocateSerialFields = useSalesOrderAllocateSerialsFields({
+ itemId: selectedLine,
+ orderId: orderId
+ });
+
+ const allocateBySerials = useCreateApiFormModal({
+ url: ApiEndpoints.sales_order_allocate_serials,
+ pk: orderId,
+ title: t`Allocate Serial Numbers`,
+ fields: allocateSerialFields,
+ table: table
+ });
+
const buildOrderFields = useBuildOrderFields({ create: true });
const newBuildOrder = useCreateApiFormModal({
@@ -264,6 +281,20 @@ export default function SalesOrderLineItemTable({
color: 'green',
onClick: notYetImplemented
},
+ {
+ hidden:
+ !record?.part_detail?.trackable ||
+ allocated ||
+ !editable ||
+ !user.hasChangeRole(UserRoles.sales_order),
+ title: t`Allocate Serials`,
+ icon: ,
+ color: 'green',
+ onClick: () => {
+ setSelectedLine(record.pk);
+ allocateBySerials.open();
+ }
+ },
{
hidden:
allocated ||
@@ -323,6 +354,7 @@ export default function SalesOrderLineItemTable({
{deleteLine.modal}
{newLine.modal}
{newBuildOrder.modal}
+ {allocateBySerials.modal}