From 4df42cd776b9061029b88a17878a06cbf526fd73 Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Mon, 4 Nov 2024 14:10:17 +1100
Subject: [PATCH] PO receive fix (#8423)

* Add "active" filter to PurchaseOrderLineItem.part field

* Fix for allocation serial numbers to incoming items

* Validate serial numbers per line item
---
 src/backend/InvenTree/order/serializers.py    | 14 +++++++++++
 .../templates/js/translated/purchase_order.js |  1 +
 src/frontend/src/forms/PurchaseOrderForms.tsx | 24 +++++++++++++++----
 .../purchasing/PurchaseOrderLineItemTable.tsx |  8 +++++--
 4 files changed, 40 insertions(+), 7 deletions(-)

diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py
index ea65d1c9be..feeb649d53 100644
--- a/src/backend/InvenTree/order/serializers.py
+++ b/src/backend/InvenTree/order/serializers.py
@@ -845,6 +845,20 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
             except DjangoValidationError as e:
                 raise ValidationError({'serial_numbers': e.messages})
 
+            invalid_serials = []
+
+            # Check the serial numbers are valid
+            for serial in data['serials']:
+                try:
+                    base_part.validate_serial_number(serial, raise_error=True)
+                except (ValidationError, DjangoValidationError):
+                    invalid_serials.append(serial)
+
+            if len(invalid_serials) > 0:
+                msg = _('The following serial numbers already exist or are invalid')
+                msg += ': ' + ', '.join(invalid_serials)
+                raise ValidationError({'serial_numbers': msg})
+
         return data
 
 
diff --git a/src/backend/InvenTree/templates/js/translated/purchase_order.js b/src/backend/InvenTree/templates/js/translated/purchase_order.js
index 170234b6bb..2ddfc71359 100644
--- a/src/backend/InvenTree/templates/js/translated/purchase_order.js
+++ b/src/backend/InvenTree/templates/js/translated/purchase_order.js
@@ -259,6 +259,7 @@ function poLineItemFields(options={}) {
         part: {
             icon: 'fa-shapes',
             filters: {
+                active: true,
                 part_detail: true,
                 supplier_detail: true,
                 supplier: options.supplier,
diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx
index e0f6b77c91..add7d92414 100644
--- a/src/frontend/src/forms/PurchaseOrderForms.tsx
+++ b/src/frontend/src/forms/PurchaseOrderForms.tsx
@@ -237,6 +237,12 @@ function LineItemFormRow({
     onClose: () => props.changeFn(props.idx, 'location', undefined)
   });
 
+  // Is this a trackable part?
+  const trackable: boolean = useMemo(
+    () => record.part_detail?.trackable ?? false,
+    [record]
+  );
+
   useEffect(() => {
     if (!!record.destination) {
       props.changeFn(props.idx, 'location', record.destination);
@@ -303,6 +309,14 @@ function LineItemFormRow({
     props.changeFn(props.idx, 'barcode', barcode);
   }, [barcode]);
 
+  const batchToolTip: string = useMemo(() => {
+    if (trackable) {
+      return t`Assign Batch Code and Serial Numbers`;
+    } else {
+      return t`Assign Batch Code`;
+    }
+  }, [trackable]);
+
   // Update location field description on state change
   useEffect(() => {
     if (!opened) {
@@ -418,9 +432,7 @@ function LineItemFormRow({
               size="sm"
               onClick={() => batchHandlers.toggle()}
               icon={<InvenTreeIcon icon="batch_code" />}
-              tooltip={t`Assign Batch Code${
-                record.trackable && ' and Serial Numbers'
-              }`}
+              tooltip={batchToolTip}
               tooltipAlignment="top"
               variant={batchOpen ? 'filled' : 'transparent'}
             />
@@ -552,18 +564,20 @@ function LineItemFormRow({
         fieldDefinition={{
           field_type: 'string',
           label: t`Batch Code`,
+          description: t`Enter batch code for received items`,
           value: props.item.batch_code
         }}
         error={props.rowErrors?.batch_code?.message}
       />
       <TableFieldExtraRow
-        visible={batchOpen && record.trackable}
+        visible={batchOpen && trackable}
         onValueChange={(value) =>
           props.changeFn(props.idx, 'serial_numbers', value)
         }
         fieldDefinition={{
           field_type: 'string',
-          label: t`Serial numbers`,
+          label: t`Serial Numbers`,
+          description: t`Enter serial numbers for received items`,
           value: props.item.serial_numbers
         }}
         error={props.rowErrors?.serial_numbers?.message}
diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
index 02ffe97978..9a97a4c961 100644
--- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
+++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
@@ -302,6 +302,10 @@ export function PurchaseOrderLineItemTable({
     );
   }, [order, poStatus]);
 
+  const orderPlaced: boolean = useMemo(() => {
+    return order.status == poStatus.PLACED;
+  }, [order, poStatus]);
+
   const rowActions = useCallback(
     (record: any): RowAction[] => {
       let received = (record?.received ?? 0) >= (record?.quantity ?? 0);
@@ -370,10 +374,10 @@ export function PurchaseOrderLineItemTable({
         icon={<IconSquareArrowRight />}
         onClick={() => receiveLineItems.open()}
         disabled={table.selectedRecords.length === 0}
-        hidden={!orderOpen || !user.hasChangeRole(UserRoles.purchase_order)}
+        hidden={!orderPlaced || !user.hasChangeRole(UserRoles.purchase_order)}
       />
     ];
-  }, [orderId, user, table, orderOpen]);
+  }, [orderId, user, table, orderOpen, orderPlaced]);
 
   return (
     <>