mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	[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
This commit is contained in:
		| @@ -1517,37 +1517,45 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer): | |||||||
|         except DjangoValidationError as e: |         except DjangoValidationError as e: | ||||||
|             raise ValidationError({'serial_numbers': e.messages}) |             raise ValidationError({'serial_numbers': e.messages}) | ||||||
|  |  | ||||||
|         serials_not_exist = [] |         serials_not_exist = set() | ||||||
|         serials_allocated = [] |         serials_unavailable = set() | ||||||
|         stock_items_to_allocate = [] |         stock_items_to_allocate = [] | ||||||
|  |  | ||||||
|         for serial in data['serials']: |         for serial in data['serials']: | ||||||
|  |             serial = str(serial).strip() | ||||||
|  |  | ||||||
|             items = stock.models.StockItem.objects.filter( |             items = stock.models.StockItem.objects.filter( | ||||||
|                 part=part, serial=serial, quantity=1 |                 part=part, serial=serial, quantity=1 | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             if not items.exists(): |             if not items.exists(): | ||||||
|                 serials_not_exist.append(str(serial)) |                 serials_not_exist.add(str(serial)) | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             stock_item = items[0] |             stock_item = items[0] | ||||||
|  |  | ||||||
|             if stock_item.unallocated_quantity() == 1: |             if not stock_item.in_stock: | ||||||
|                 stock_items_to_allocate.append(stock_item) |                 serials_unavailable.add(str(serial)) | ||||||
|             else: |                 continue | ||||||
|                 serials_allocated.append(str(serial)) |  | ||||||
|  |             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: |         if len(serials_not_exist) > 0: | ||||||
|             error_msg = _('No match found for the following serial numbers') |             error_msg = _('No match found for the following serial numbers') | ||||||
|             error_msg += ': ' |             error_msg += ': ' | ||||||
|             error_msg += ','.join(serials_not_exist) |             error_msg += ','.join(sorted(serials_not_exist)) | ||||||
|  |  | ||||||
|             raise ValidationError({'serial_numbers': error_msg}) |             raise ValidationError({'serial_numbers': error_msg}) | ||||||
|  |  | ||||||
|         if len(serials_allocated) > 0: |         if len(serials_unavailable) > 0: | ||||||
|             error_msg = _('The following serial numbers are already allocated') |             error_msg = _('The following serial numbers are unavailable') | ||||||
|             error_msg += ': ' |             error_msg += ': ' | ||||||
|             error_msg += ','.join(serials_allocated) |             error_msg += ','.join(sorted(serials_unavailable)) | ||||||
|  |  | ||||||
|             raise ValidationError({'serial_numbers': error_msg}) |             raise ValidationError({'serial_numbers': error_msg}) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -152,6 +152,7 @@ export enum ApiEndpoints { | |||||||
|   sales_order_extra_line_list = 'order/so-extra-line/', |   sales_order_extra_line_list = 'order/so-extra-line/', | ||||||
|   sales_order_allocation_list = 'order/so-allocation/', |   sales_order_allocation_list = 'order/so-allocation/', | ||||||
|   sales_order_shipment_list = 'order/so/shipment/', |   sales_order_shipment_list = 'order/so/shipment/', | ||||||
|  |   sales_order_allocate_serials = 'order/so/:id/allocate-serials/', | ||||||
|  |  | ||||||
|   return_order_list = 'order/ro/', |   return_order_list = 'order/ro/', | ||||||
|   return_order_issue = 'order/ro/:id/issue/', |   return_order_issue = 'order/ro/:id/issue/', | ||||||
|   | |||||||
| @@ -84,6 +84,31 @@ export function useSalesOrderLineItemFields({ | |||||||
|   return fields; |   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 { | export function useSalesOrderShipmentFields(): ApiFormFieldSet { | ||||||
|   return useMemo(() => { |   return useMemo(() => { | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { t } from '@lingui/macro'; | import { t } from '@lingui/macro'; | ||||||
| import { Text } from '@mantine/core'; | import { Text } from '@mantine/core'; | ||||||
| import { | import { | ||||||
|  |   IconHash, | ||||||
|   IconShoppingCart, |   IconShoppingCart, | ||||||
|   IconSquareArrowRight, |   IconSquareArrowRight, | ||||||
|   IconTools |   IconTools | ||||||
| @@ -14,7 +15,10 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; | |||||||
| import { ModelType } from '../../enums/ModelType'; | import { ModelType } from '../../enums/ModelType'; | ||||||
| import { UserRoles } from '../../enums/Roles'; | import { UserRoles } from '../../enums/Roles'; | ||||||
| import { useBuildOrderFields } from '../../forms/BuildForms'; | import { useBuildOrderFields } from '../../forms/BuildForms'; | ||||||
| import { useSalesOrderLineItemFields } from '../../forms/SalesOrderForms'; | import { | ||||||
|  |   useSalesOrderAllocateSerialsFields, | ||||||
|  |   useSalesOrderLineItemFields | ||||||
|  | } from '../../forms/SalesOrderForms'; | ||||||
| import { notYetImplemented } from '../../functions/notifications'; | import { notYetImplemented } from '../../functions/notifications'; | ||||||
| import { | import { | ||||||
|   useCreateApiFormModal, |   useCreateApiFormModal, | ||||||
| @@ -223,6 +227,19 @@ export default function SalesOrderLineItemTable({ | |||||||
|     table: table |     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 buildOrderFields = useBuildOrderFields({ create: true }); | ||||||
|  |  | ||||||
|   const newBuildOrder = useCreateApiFormModal({ |   const newBuildOrder = useCreateApiFormModal({ | ||||||
| @@ -264,6 +281,20 @@ export default function SalesOrderLineItemTable({ | |||||||
|           color: 'green', |           color: 'green', | ||||||
|           onClick: notYetImplemented |           onClick: notYetImplemented | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |           hidden: | ||||||
|  |             !record?.part_detail?.trackable || | ||||||
|  |             allocated || | ||||||
|  |             !editable || | ||||||
|  |             !user.hasChangeRole(UserRoles.sales_order), | ||||||
|  |           title: t`Allocate Serials`, | ||||||
|  |           icon: <IconHash />, | ||||||
|  |           color: 'green', | ||||||
|  |           onClick: () => { | ||||||
|  |             setSelectedLine(record.pk); | ||||||
|  |             allocateBySerials.open(); | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|           hidden: |           hidden: | ||||||
|             allocated || |             allocated || | ||||||
| @@ -323,6 +354,7 @@ export default function SalesOrderLineItemTable({ | |||||||
|       {deleteLine.modal} |       {deleteLine.modal} | ||||||
|       {newLine.modal} |       {newLine.modal} | ||||||
|       {newBuildOrder.modal} |       {newBuildOrder.modal} | ||||||
|  |       {allocateBySerials.modal} | ||||||
|       <InvenTreeTable |       <InvenTreeTable | ||||||
|         url={apiUrl(ApiEndpoints.sales_order_line_list)} |         url={apiUrl(ApiEndpoints.sales_order_line_list)} | ||||||
|         tableState={table} |         tableState={table} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user