From 67fd6a564a63d2ca6aa0e0e06cb12614c929f487 Mon Sep 17 00:00:00 2001
From: Bobbe <34186858+30350n@users.noreply.github.com>
Date: Thu, 19 Oct 2023 14:28:32 +0200
Subject: [PATCH] Add POReceiveBarcodeHandler to support barcode/po-receive/
 endpoint (#421)

* Add POReceiveBarcodeHandler to support barcode/po-receive/ endpoint

* Remove german translation

* Add api version checks

* Add getOverlayText method to barcode handler

* Bump required API version to 139

* Update barcode.dart

The "quantity" field is not an integer, and can cause the app to crash if not handled correctly

---------

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
---
 lib/api.dart                          |   2 +
 lib/barcode/barcode.dart              | 111 ++++++++++++++++++++++++++
 lib/barcode/handler.dart              |   6 +-
 lib/l10n/app_en.arb                   |   6 ++
 lib/widget/location_display.dart      |  15 ++++
 lib/widget/purchase_order_detail.dart |  24 ++++++
 lib/widget/purchase_order_list.dart   |  23 ++++++
 7 files changed, 185 insertions(+), 2 deletions(-)

diff --git a/lib/api.dart b/lib/api.dart
index ba641710..6ea78805 100644
--- a/lib/api.dart
+++ b/lib/api.dart
@@ -328,6 +328,8 @@ class InvenTreeAPI {
   // Does the server support extra fields on stock adjustment actions?
   bool get supportsStockAdjustExtraFields => isConnected() && apiVersion >= 133;
 
+  bool get supportsBarcodePOReceiveEndpoint => isConnected() && apiVersion >= 139;
+
   // Are plugins enabled on the server?
   bool _pluginsEnabled = false;
 
diff --git a/lib/barcode/barcode.dart b/lib/barcode/barcode.dart
index d87f34c3..6a2a069c 100644
--- a/lib/barcode/barcode.dart
+++ b/lib/barcode/barcode.dart
@@ -6,6 +6,7 @@ import "package:one_context/one_context.dart";
 
 
 import "package:inventree/api.dart";
+import "package:inventree/api_form.dart";
 import "package:inventree/helpers.dart";
 import "package:inventree/l10.dart";
 
@@ -462,6 +463,116 @@ class ScanParentLocationHandler extends BarcodeScanStockLocationHandler {
 }
 
 
+/*
+ * Barcode handler class for scanning a supplier barcode to receive a part
+ *
+ * - The class can be initialized by optionally passing a valid, placed PurchaseOrder object
+ * - Expects to scan supplier barcode, possibly containing order_number and quantity
+ * - If location or quantity information wasn't provided, show a form to fill it in
+ */
+class POReceiveBarcodeHandler extends BarcodeHandler {
+
+  POReceiveBarcodeHandler({this.purchaseOrder, this.location});
+
+  InvenTreePurchaseOrder? purchaseOrder;
+  InvenTreeStockLocation? location;
+
+  @override
+  String getOverlayText(BuildContext context) => L10().barcodeReceivePart;
+
+  @override
+  Future<void> processBarcode(String barcode,
+      {String url = "barcode/po-receive/",
+      Map<String, dynamic> extra_data = const {}}) {
+
+    final po_extra_data = {
+      "purchase_order": purchaseOrder?.pk,
+      "location": location?.pk,
+      ...extra_data,
+    };
+
+    return super.processBarcode(barcode, url: url, extra_data: po_extra_data);
+  }
+
+  @override
+  Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
+      if (!data.containsKey("lineitem")) {
+        return onBarcodeUnknown(data);
+      }
+
+      barcodeSuccessTone();
+      showSnackIcon(L10().receivedItem, success: true);
+  }
+
+  @override
+  Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
+    if (!data.containsKey("action_required") || !data.containsKey("lineitem")) {
+      return super.onBarcodeUnhandled(data);
+    }
+
+    final lineItemData = data["lineitem"] as Map<String, dynamic>;
+    if (!lineItemData.containsKey("pk") || !lineItemData.containsKey("purchase_order")) {
+      barcodeFailureTone();
+      showSnackIcon(L10().missingData, success: false);
+    }
+
+    // Construct fields to receive
+    Map<String, dynamic> fields = {
+      "line_item": {
+        "parent": "items",
+        "nested": true,
+        "hidden": true,
+        "value": lineItemData["pk"] as int,
+      },
+      "quantity": {
+        "parent": "items",
+        "nested": true,
+        "value": lineItemData["quantity"] as double?,
+      },
+      "status": {
+        "parent": "items",
+        "nested": true,
+      },
+      "location": {
+        "value": lineItemData["location"] as int?,
+      },
+      "barcode": {
+        "parent": "items",
+        "nested": true,
+        "hidden": true,
+        "type": "barcode",
+        "value": data["barcode_data"] as String,
+      }
+    };
+
+    final context = OneContext().context!;
+    final purchase_order_pk = lineItemData["purchase_order"];
+    final receive_url = "${InvenTreePurchaseOrder().URL}${purchase_order_pk}/receive/";
+
+    launchApiForm(
+      context,
+      L10().receiveItem,
+      receive_url,
+      fields,
+      method: "POST",
+      icon: FontAwesomeIcons.rightToBracket,
+      onSuccess: (data) async {
+        showSnackIcon(L10().receivedItem, success: true);
+      }
+    );
+  }
+
+  @override
+  Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
+    barcodeFailureTone();
+    showSnackIcon(
+      data["error"] as String? ?? L10().barcodeError,
+      success: false
+    );
+  }
+}
+
+
 /*
  * Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
  */
diff --git a/lib/barcode/handler.dart b/lib/barcode/handler.dart
index 3d72e9c9..5048f635 100644
--- a/lib/barcode/handler.dart
+++ b/lib/barcode/handler.dart
@@ -58,8 +58,9 @@ class BarcodeHandler {
     *
     * Returns true only if the barcode scanner should remain open
     */
-  Future<void> processBarcode(String barcode, {String url = "barcode/"}) async {
-
+  Future<void> processBarcode(String barcode,
+      {String url = "barcode/",
+      Map<String, dynamic> extra_data = const {}}) async {
     debug("Scanned barcode data: '${barcode}'");
 
     barcode = barcode.trim();
@@ -82,6 +83,7 @@ class BarcodeHandler {
         url,
         body: {
           "barcode": barcode,
+          ...extra_data,
         },
         expectedStatusCode: null,  // Do not show an error on "unexpected code"
     );
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 9589f9ba..673d284f 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -112,6 +112,9 @@
   "barcodeNotAssigned": "Barcode not assigned",
   "@barcodeNotAssigned": {},
 
+  "barcodeReceivePart": "Scan barcode to receive part",
+  "@barcodeReceivePart": {},
+
   "barcodeScanAssign": "Scan to assign barcode",
   "@barcodeScanAssign": {},
 
@@ -988,6 +991,9 @@
   "scanIntoLocationDetail": "Scan this item into location",
   "@scanIntoLocationDetail": {},
 
+  "scanReceivedParts": "Scan Received Parts",
+  "@scanReceivedParts": {},
+
   "search": "Search",
   "@search": {
     "description": "search"
diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart
index 0ca1c5dd..4e89f86a 100644
--- a/lib/widget/location_display.dart
+++ b/lib/widget/location_display.dart
@@ -104,6 +104,21 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
         );
       }
 
+      if (api.supportsBarcodePOReceiveEndpoint) {
+        actions.add(
+          SpeedDialChild(
+            child: Icon(Icons.barcode_reader),
+            label: L10().scanReceivedParts,
+            onTap:() async {
+              scanBarcode(
+                context,
+                handler: POReceiveBarcodeHandler(location: location),
+              );
+            },
+          )
+        );
+      }
+
       // Scan this location into another one
       if (InvenTreeStockLocation().canEdit) {
         actions.add(
diff --git a/lib/widget/purchase_order_detail.dart b/lib/widget/purchase_order_detail.dart
index ba870a49..f03d4f0b 100644
--- a/lib/widget/purchase_order_detail.dart
+++ b/lib/widget/purchase_order_detail.dart
@@ -6,6 +6,7 @@ import "package:inventree/widget/po_line_list.dart";
 
 import "package:inventree/api.dart";
 import "package:inventree/app_colors.dart";
+import "package:inventree/barcode/barcode.dart";
 import "package:inventree/helpers.dart";
 import "package:inventree/l10.dart";
 
@@ -132,6 +133,29 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
     );
   }
 
+  @override
+  List<SpeedDialChild> barcodeButtons(BuildContext context) {
+    List<SpeedDialChild> actions = [];
+
+    if (api.supportsBarcodePOReceiveEndpoint) {
+      actions.add(
+        SpeedDialChild(
+          child: Icon(Icons.barcode_reader),
+          label: L10().scanReceivedParts,
+          onTap:() async {
+            scanBarcode(
+              context,
+              handler: POReceiveBarcodeHandler(purchaseOrder: order),
+            );
+          },
+        )
+      );
+    }
+
+    return actions;
+  }
+
+
   @override
   Future<void> request(BuildContext context) async {
     await order.reload();
diff --git a/lib/widget/purchase_order_list.dart b/lib/widget/purchase_order_list.dart
index cdb8b8be..44046564 100644
--- a/lib/widget/purchase_order_list.dart
+++ b/lib/widget/purchase_order_list.dart
@@ -9,6 +9,7 @@ import "package:inventree/widget/purchase_order_detail.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/l10.dart";
 import "package:inventree/api.dart";
+import "package:inventree/barcode/barcode.dart";
 import "package:inventree/inventree/purchase_order.dart";
 
 /*
@@ -79,6 +80,28 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
     );
   }
 
+  @override
+  List<SpeedDialChild> barcodeButtons(BuildContext context) {
+    List<SpeedDialChild> actions = [];
+
+    if (api.supportsBarcodePOReceiveEndpoint) {
+      actions.add(
+        SpeedDialChild(
+          child: Icon(Icons.barcode_reader),
+          label: L10().scanReceivedParts,
+          onTap:() async {
+            scanBarcode(
+              context,
+              handler: POReceiveBarcodeHandler(),
+            );
+          },
+        )
+      );
+    }
+
+    return actions;
+  }
+
   @override
   Widget getBody(BuildContext context) {
     return PaginatedPurchaseOrderList(filters);