From 9f92475af0b0148edb62ee7c929a9e48deae2f22 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 6 Sep 2024 14:33:16 +1000 Subject: [PATCH] [PUI] Sales order actions (#8086) * [PUI] Add placeholder action - "Allocate Serials" action for sales order - No functionality yet * Implement form for allocating by serial numbers * Improve validation of serial numbers in back-end * Trim serial number string --- src/backend/InvenTree/order/serializers.py | 30 ++++++++++------ src/frontend/src/enums/ApiEndpoints.tsx | 1 + src/frontend/src/forms/SalesOrderForms.tsx | 25 ++++++++++++++ .../tables/sales/SalesOrderLineItemTable.tsx | 34 ++++++++++++++++++- 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 2698579273ef..2060c7b4b0f6 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -1517,37 +1517,45 @@ def validate(self, data): 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 6ce43aa91079..17a981b9786c 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 02f5a976d5d1..d67ede312bb9 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 240e3a5ccff7..ea2e91358a78 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}