mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 05:15:42 +00:00 
			
		
		
		
	[refactor] Scan improvements (#577)
* Handle error on unexpected barcode response * Add ManufacturerPart detail view * Support barcode scanning for manufacturer part * Refactoring for null checks * Ignore selected errors in sentry * Fix API implementation for ManufacturerPart * Update release notes * More error handling * Decode quantity betterer * Refactoring * Add option to confirm checkin details * Improve response handlign * Cleanup * Remove unused imports * Fix async function * Fix for assigning custom barcode * Handle barcode scan result for company * Fix * Adjust scan priority * Refactoring MODEL_TYPE - Use instead of duplicated const strings * @override fix
This commit is contained in:
		| @@ -4,7 +4,9 @@ import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
| import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; | ||||
| import "package:inventree/helpers.dart"; | ||||
| import "package:inventree/inventree/sales_order.dart"; | ||||
| import "package:inventree/inventree/sentry.dart"; | ||||
| import "package:inventree/preferences.dart"; | ||||
| import "package:inventree/widget/company/manufacturer_part_detail.dart"; | ||||
| import "package:inventree/widget/order/sales_order_detail.dart"; | ||||
| import "package:one_context/one_context.dart"; | ||||
|  | ||||
| @@ -30,6 +32,7 @@ import "package:inventree/widget/order/purchase_order_detail.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/widget/stock/stock_detail.dart"; | ||||
| import "package:inventree/widget/company/company_detail.dart"; | ||||
| import "package:inventree/widget/company/supplier_part_detail.dart"; | ||||
|  | ||||
|  | ||||
| @@ -176,15 +179,37 @@ class BarcodeScanHandler extends BarcodeHandler { | ||||
|    */ | ||||
|   Future<void> handleSupplierPart(int pk) async { | ||||
|  | ||||
|     var supplierpart = await InvenTreeSupplierPart().get(pk); | ||||
|     var supplierPart = await InvenTreeSupplierPart().get(pk); | ||||
|  | ||||
|     if (supplierpart is InvenTreeSupplierPart) { | ||||
|     if (supplierPart is InvenTreeSupplierPart) { | ||||
|       OneContext().pop(); | ||||
|       OneContext().push(MaterialPageRoute( | ||||
|           builder: (context) => SupplierPartDetailWidget(supplierpart))); | ||||
|           builder: (context) => SupplierPartDetailWidget(supplierPart))); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|     * Response when a "ManufacturerPart" instance is scanned | ||||
|    */ | ||||
|   Future<void> handleManufacturerPart(int pk) async { | ||||
|     var manufacturerPart = await InvenTreeManufacturerPart().get(pk); | ||||
|  | ||||
|     if (manufacturerPart is InvenTreeManufacturerPart) { | ||||
|       OneContext().pop(); | ||||
|       OneContext().push(MaterialPageRoute( | ||||
|           builder: (context) => ManufacturerPartDetailWidget(manufacturerPart))); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> handleCompany(int pk) async { | ||||
|     var company = await InvenTreeCompany().get(pk); | ||||
|  | ||||
|     if (company is InvenTreeCompany) { | ||||
|       OneContext().pop(); | ||||
|       OneContext().push(MaterialPageRoute( | ||||
|           builder: (context) => CompanyDetailWidget(company))); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Response when a "PurchaseOrder" instance is scanned | ||||
| @@ -218,26 +243,32 @@ class BarcodeScanHandler extends BarcodeHandler { | ||||
|  | ||||
|     // The following model types can be matched with barcodes | ||||
|     List<String> validModels = [ | ||||
|       "part", | ||||
|       "stockitem", | ||||
|       "stocklocation", | ||||
|       "supplierpart", | ||||
|       InvenTreePart.MODEL_TYPE, | ||||
|       InvenTreeCompany.MODEL_TYPE, | ||||
|       InvenTreeStockItem.MODEL_TYPE, | ||||
|       InvenTreeStockLocation.MODEL_TYPE, | ||||
|       InvenTreeSupplierPart.MODEL_TYPE, | ||||
|       InvenTreeManufacturerPart.MODEL_TYPE, | ||||
|     ]; | ||||
|  | ||||
|  | ||||
|     if (InvenTreeAPI().supportsOrderBarcodes) { | ||||
|       validModels.add("purchaseorder"); | ||||
|       validModels.add("salesorder"); | ||||
|       validModels.add(InvenTreePurchaseOrder.MODEL_TYPE); | ||||
|       validModels.add(InvenTreeSalesOrder.MODEL_TYPE); | ||||
|     } | ||||
|  | ||||
|     for (var key in validModels) { | ||||
|       if (data.containsKey(key)) { | ||||
|         pk = (data[key]?["pk"] ?? -1) as int; | ||||
|         try { | ||||
|           pk = (data[key]?["pk"] ?? -1) as int; | ||||
|  | ||||
|         // Break on the first valid match found | ||||
|         if (pk > 0) { | ||||
|           model = key; | ||||
|           break; | ||||
|           // Break on the first valid match found | ||||
|           if (pk > 0) { | ||||
|             model = key; | ||||
|             break; | ||||
|           } | ||||
|         } catch (error, stackTrace) { | ||||
|           sentryReportError("onBarcodeMatched", error, stackTrace); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -248,25 +279,30 @@ class BarcodeScanHandler extends BarcodeHandler { | ||||
|       barcodeSuccessTone(); | ||||
|  | ||||
|       switch (model) { | ||||
|         case "part": | ||||
|           await handlePart(pk); | ||||
|           return; | ||||
|         case "stockitem": | ||||
|         case InvenTreeStockItem.MODEL_TYPE: | ||||
|           await handleStockItem(pk); | ||||
|           return; | ||||
|         case "stocklocation": | ||||
|           await handleStockLocation(pk); | ||||
|           return; | ||||
|         case "supplierpart": | ||||
|           await handleSupplierPart(pk); | ||||
|           return; | ||||
|         case "purchaseorder": | ||||
|         case InvenTreePurchaseOrder.MODEL_TYPE: | ||||
|           await handlePurchaseOrder(pk); | ||||
|           return; | ||||
|         case "salesorder": | ||||
|         case InvenTreeSalesOrder.MODEL_TYPE: | ||||
|           await handleSalesOrder(pk); | ||||
|           return; | ||||
|           // TODO: Handle manufacturer part | ||||
|         case InvenTreeStockLocation.MODEL_TYPE: | ||||
|           await handleStockLocation(pk); | ||||
|           return; | ||||
|         case InvenTreeSupplierPart.MODEL_TYPE: | ||||
|           await handleSupplierPart(pk); | ||||
|           return; | ||||
|         case InvenTreeManufacturerPart.MODEL_TYPE: | ||||
|           await handleManufacturerPart(pk); | ||||
|           return; | ||||
|         case InvenTreePart.MODEL_TYPE: | ||||
|           await handlePart(pk); | ||||
|           return; | ||||
|         case InvenTreeCompany.MODEL_TYPE: | ||||
|           await handleCompany(pk); | ||||
|           return; | ||||
|         default: | ||||
|           // Fall through to failure state | ||||
|           break; | ||||
| @@ -324,21 +360,6 @@ class UniqueBarcodeHandler extends BarcodeHandler { | ||||
|  | ||||
|   @override | ||||
|   Future<void> onBarcodeMatched(Map<String, dynamic> data) async { | ||||
|  | ||||
|     barcodeFailureTone(); | ||||
|  | ||||
|     // If the barcode is known, we can"t assign it to the stock item! | ||||
|     showSnackIcon( | ||||
|         L10().barcodeInUse, | ||||
|         icon: Icons.qr_code, | ||||
|         success: false | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> onBarcodeUnknown(Map<String, dynamic> data) async { | ||||
|     // If the barcode is unknown, we *can* assign it to the stock item! | ||||
|  | ||||
|     if (!data.containsKey("hash") && !data.containsKey("barcode_hash")) { | ||||
|       showServerError( | ||||
|         "barcode/", | ||||
| @@ -370,6 +391,12 @@ class UniqueBarcodeHandler extends BarcodeHandler { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> onBarcodeUnknown(Map<String, dynamic> data) async { | ||||
|     await onBarcodeMatched(data); | ||||
|   } | ||||
|    | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -93,7 +93,7 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState { | ||||
|   /* | ||||
|    * Callback function when a barcode is scanned | ||||
|    */ | ||||
|   void _onScanSuccess(Code? code) { | ||||
|   Future<void> onScanSuccess(Code? code) async { | ||||
|  | ||||
|     if (scanning_paused) { | ||||
|       return; | ||||
| @@ -122,18 +122,16 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState { | ||||
|     if (mounted) { | ||||
|       setState(() { | ||||
|         scanned_code = barcode; | ||||
|         scanning_paused = barcode.isNotEmpty; | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (barcode.isNotEmpty) { | ||||
|  | ||||
|       handleBarcodeData(barcode).then((_) { | ||||
|       pauseScan(); | ||||
|  | ||||
|       await handleBarcodeData(barcode).then((_) { | ||||
|         if (!single_scanning && mounted) { | ||||
|           // Resume next scan | ||||
|           setState(() { | ||||
|             scanning_paused = false; | ||||
|           }); | ||||
|           resumeScan(); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
| @@ -186,7 +184,7 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState { | ||||
|   Widget BarcodeReader(BuildContext context) { | ||||
|  | ||||
|     return ReaderWidget( | ||||
|       onScan: _onScanSuccess, | ||||
|       onScan: onScanSuccess, | ||||
|       isMultiScan: false, | ||||
|       tryHarder: true, | ||||
|       tryInverted: true, | ||||
|   | ||||
| @@ -79,14 +79,27 @@ class BarcodeHandler { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     var response = await InvenTreeAPI().post( | ||||
|     APIResponse? response; | ||||
|  | ||||
|     try { | ||||
|       response = await InvenTreeAPI().post( | ||||
|         url, | ||||
|         body: { | ||||
|           "barcode": barcode, | ||||
|           ...extra_data, | ||||
|         }, | ||||
|         expectedStatusCode: null,  // Do not show an error on "unexpected code" | ||||
|     ); | ||||
|         expectedStatusCode: null, // Do not show an error on "unexpected code" | ||||
|       ); | ||||
|     } catch (error, stackTrace) { | ||||
|       sentryReportError("Barcode.processBarcode", error, stackTrace); | ||||
|       response = null; | ||||
|     } | ||||
|  | ||||
|     if (response == null) { | ||||
|       barcodeFailureTone(); | ||||
|       showSnackIcon(L10().barcodeError, success: false); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     debug("Barcode scan response" + response.data.toString()); | ||||
|  | ||||
| @@ -94,7 +107,7 @@ class BarcodeHandler { | ||||
|  | ||||
|     // Handle strange response from the server | ||||
|     if (!response.isValid() || !response.isMap()) { | ||||
|       onBarcodeUnknown({}); | ||||
|       await onBarcodeUnknown({}); | ||||
|  | ||||
|       showSnackIcon(L10().serverError, success: false); | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; | ||||
| import "package:inventree/preferences.dart"; | ||||
| import "package:one_context/one_context.dart"; | ||||
|  | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/api_form.dart"; | ||||
|  | ||||
| import "package:inventree/barcode/barcode.dart"; | ||||
| import "package:inventree/barcode/handler.dart"; | ||||
| @@ -23,10 +21,11 @@ import "package:inventree/widget/snacks.dart"; | ||||
|  */ | ||||
| class POReceiveBarcodeHandler extends BarcodeHandler { | ||||
|  | ||||
|   POReceiveBarcodeHandler({this.purchaseOrder, this.location}); | ||||
|   POReceiveBarcodeHandler({this.purchaseOrder, this.location, this.lineItem}); | ||||
|  | ||||
|   InvenTreePurchaseOrder? purchaseOrder; | ||||
|   InvenTreeStockLocation? location; | ||||
|   InvenTreePOLineItem? lineItem; | ||||
|  | ||||
|   @override | ||||
|   String getOverlayText(BuildContext context) => L10().barcodeReceivePart; | ||||
| @@ -34,11 +33,15 @@ class POReceiveBarcodeHandler extends BarcodeHandler { | ||||
|   @override | ||||
|   Future<void> processBarcode(String barcode, | ||||
|       {String url = "barcode/po-receive/", | ||||
|         Map<String, dynamic> extra_data = const {}}) { | ||||
|         Map<String, dynamic> extra_data = const {}}) async { | ||||
|  | ||||
|     final bool confirm = await InvenTreeSettingsManager().getBool(INV_PO_CONFIRM_SCAN, true); | ||||
|  | ||||
|     final po_extra_data = { | ||||
|       "purchase_order": purchaseOrder?.pk, | ||||
|       "location": location?.pk, | ||||
|       "line_item": lineItem?.pk, | ||||
|       "auto_allocate": !confirm, | ||||
|       ...extra_data, | ||||
|     }; | ||||
|  | ||||
| @@ -47,11 +50,13 @@ class POReceiveBarcodeHandler extends BarcodeHandler { | ||||
|  | ||||
|   @override | ||||
|   Future<void> onBarcodeMatched(Map<String, dynamic> data) async { | ||||
|     if (!data.containsKey("lineitem")) { | ||||
|  | ||||
|     if (data.containsKey("lineitem") || data.containsKey("success")) { | ||||
|       barcodeSuccess(L10().receivedItem); | ||||
|       return; | ||||
|     } else { | ||||
|       return onBarcodeUnknown(data); | ||||
|     } | ||||
|  | ||||
|     barcodeSuccess(L10().receivedItem); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -66,49 +71,41 @@ class POReceiveBarcodeHandler extends BarcodeHandler { | ||||
|       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, | ||||
|     // At minimum, we need the line item ID value | ||||
|     final int? lineItemId = lineItemData["pk"] as int?; | ||||
|  | ||||
|     if (lineItemId == null) { | ||||
|       barcodeFailureTone(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     InvenTreePOLineItem? lineItem = await InvenTreePOLineItem().get(lineItemId) as InvenTreePOLineItem?; | ||||
|  | ||||
|     if (lineItem == null) { | ||||
|       barcodeFailureTone(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Next, extract the "optional" fields | ||||
|  | ||||
|     // Extract information from the returned server response | ||||
|     double? quantity = double.tryParse((lineItemData["quantity"] ?? "0").toString()); | ||||
|     int? destination = lineItemData["location"] as int?; | ||||
|     String? barcode = data["barcode_data"] as String?; | ||||
|  | ||||
|     // Discard the barcode scanner at this stage | ||||
|     if (OneContext.hasContext) { | ||||
|       OneContext().pop(); | ||||
|     } | ||||
|  | ||||
|     await lineItem.receive( | ||||
|       OneContext().context!, | ||||
|       destination: destination, | ||||
|       quantity: quantity, | ||||
|       barcode: barcode, | ||||
|       onSuccess: () { | ||||
|         showSnackIcon(L10().receivedItem, success: true); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     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: TablerIcons.transition_right, | ||||
|         onSuccess: (data) async { | ||||
|           showSnackIcon(L10().receivedItem, success: true); | ||||
|         } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class BarcodeScanStockLocationHandler extends BarcodeHandler { | ||||
|  | ||||
|     // We expect that the barcode points to a 'stocklocation' | ||||
|     if (data.containsKey("stocklocation")) { | ||||
|       int _loc = (data["stocklocation"]["pk"] ?? -1) as int; | ||||
|       int _loc = (data["stocklocation"]?["pk"] ?? -1) as int; | ||||
|  | ||||
|       // A valid stock location! | ||||
|       if (_loc > 0) { | ||||
| @@ -83,7 +83,7 @@ class BarcodeScanStockItemHandler extends BarcodeHandler { | ||||
|   Future<void> onBarcodeMatched(Map<String, dynamic> data) async { | ||||
|     // We expect that the barcode points to a 'stockitem' | ||||
|     if (data.containsKey("stockitem")) { | ||||
|       int _item = (data["stockitem"]["pk"] ?? -1) as int; | ||||
|       int _item = (data["stockitem"]?["pk"] ?? -1) as int; | ||||
|  | ||||
|       // A valid stock location! | ||||
|       if (_item > 0) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user