mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-30 21:05:42 +00:00 
			
		
		
		
	New barcode actions (#218)
* Bump release notes * Adds method for linking custom barcodes * Custom getter method for determining if an item has barcode data * Add method to check if the API supports "modern" barcodes * Refactor custom barcode implementation for StockItem - Needs testing * Unit testing for linking and unlinking barcodes * Fixes * Refactor code for "custom barcode action" tile * Add custom barcode action to StockLocation * Add extra debug to debug the debugging * Unit test fix * Change scope I guess? * remove handler test
This commit is contained in:
		| @@ -1,9 +1,11 @@ | |||||||
| ## InvenTree App Release Notes | ## InvenTree App Release Notes | ||||||
| --- | --- | ||||||
|  |  | ||||||
| ### - December 2022 | ### 0.9.0 - November 2022 | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | - Added support for custom barcodes for Parts | ||||||
|  | - Added support for custom barcode for Stock Locations | ||||||
| - Support Part parameters | - Support Part parameters | ||||||
| - Add support for structural part categories | - Add support for structural part categories | ||||||
| - Add support for structural stock locations | - Add support for structural stock locations | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								lib/api.dart
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								lib/api.dart
									
									
									
									
									
								
							| @@ -259,6 +259,9 @@ class InvenTreeAPI { | |||||||
|   // Notification support requires API v25 or newer |   // Notification support requires API v25 or newer | ||||||
|   bool get supportsNotifications => isConnected() && apiVersion >= 25; |   bool get supportsNotifications => isConnected() && apiVersion >= 25; | ||||||
|  |  | ||||||
|  |   // Supports 'modern' barcode API (v80 or newer) | ||||||
|  |   bool get supportModernBarcodes => isConnected() && apiVersion >= 80; | ||||||
|  |  | ||||||
|   // Structural categories requires API v83 or newer |   // Structural categories requires API v83 or newer | ||||||
|   bool get supportsStructuralCategories => isConnected() && apiVersion >= 83; |   bool get supportsStructuralCategories => isConnected() && apiVersion >= 83; | ||||||
|  |  | ||||||
| @@ -883,6 +886,27 @@ class InvenTreeAPI { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /* | ||||||
|  |    * Perform a request to link a custom barcode to a particular item | ||||||
|  |    */ | ||||||
|  |   Future<bool> linkBarcode(Map<String, String> body) async { | ||||||
|  |  | ||||||
|  |   HttpClientRequest? request = await apiRequest("/barcode/link/", "POST"); | ||||||
|  |  | ||||||
|  |   if (request == null) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   final response = await completeRequest( | ||||||
|  |     request, | ||||||
|  |     data: json.encode(body), | ||||||
|  |     statusCode: 200 | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return response.isValid() && response.statusCode == 200; | ||||||
|  |  | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * Perform a request to unlink a custom barcode from a particular item |    * Perform a request to unlink a custom barcode from a particular item | ||||||
|    */ |    */ | ||||||
| @@ -1255,6 +1279,7 @@ class InvenTreeAPI { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Return True if the API supports 'settings' (requires API v46) | ||||||
|   bool get supportsSettings => isConnected() && apiVersion >= 46; |   bool get supportsSettings => isConnected() && apiVersion >= 46; | ||||||
|  |  | ||||||
|   // Keep a record of which settings we have received from the server |   // Keep a record of which settings we have received from the server | ||||||
|   | |||||||
| @@ -1,21 +1,21 @@ | |||||||
| import "dart:io"; | import "dart:io"; | ||||||
|  |  | ||||||
| import "package:inventree/inventree/sentry.dart"; |  | ||||||
| import "package:inventree/widget/dialogs.dart"; |  | ||||||
| import "package:inventree/widget/snacks.dart"; |  | ||||||
| import "package:flutter/material.dart"; | import "package:flutter/material.dart"; | ||||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||||
| import "package:one_context/one_context.dart"; | import "package:one_context/one_context.dart"; | ||||||
|  |  | ||||||
| import "package:qr_code_scanner/qr_code_scanner.dart"; | import "package:qr_code_scanner/qr_code_scanner.dart"; | ||||||
|  |  | ||||||
| import "package:inventree/inventree/stock.dart"; | import "package:inventree/app_colors.dart"; | ||||||
| import "package:inventree/inventree/part.dart"; |  | ||||||
| import "package:inventree/api.dart"; | import "package:inventree/api.dart"; | ||||||
| import "package:inventree/helpers.dart"; | import "package:inventree/helpers.dart"; | ||||||
| import "package:inventree/l10.dart"; | import "package:inventree/l10.dart"; | ||||||
| import "package:inventree/preferences.dart"; | import "package:inventree/preferences.dart"; | ||||||
|  |  | ||||||
|  | import "package:inventree/inventree/sentry.dart"; | ||||||
|  | import "package:inventree/inventree/stock.dart"; | ||||||
|  | import "package:inventree/inventree/part.dart"; | ||||||
|  |  | ||||||
|  | import "package:inventree/widget/dialogs.dart"; | ||||||
|  | import "package:inventree/widget/snacks.dart"; | ||||||
| import "package:inventree/widget/location_display.dart"; | import "package:inventree/widget/location_display.dart"; | ||||||
| import "package:inventree/widget/part_detail.dart"; | import "package:inventree/widget/part_detail.dart"; | ||||||
| import "package:inventree/widget/stock_detail.dart"; | import "package:inventree/widget/stock_detail.dart"; | ||||||
| @@ -117,6 +117,8 @@ class BarcodeHandler { | |||||||
|           expectedStatusCode: null,  // Do not show an error on "unexpected code" |           expectedStatusCode: null,  // Do not show an error on "unexpected code" | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|  |       debug("Barcode scan response" + response.data.toString()); | ||||||
|  |  | ||||||
|       _controller?.resumeCamera(); |       _controller?.resumeCamera(); | ||||||
|  |  | ||||||
|       Map<String, dynamic> data = response.asMap(); |       Map<String, dynamic> data = response.asMap(); | ||||||
| @@ -726,8 +728,57 @@ class _QRViewState extends State<InvenTreeQRView> { | |||||||
| } | } | ||||||
|  |  | ||||||
| Future<void> scanQrCode(BuildContext context) async { | Future<void> scanQrCode(BuildContext context) async { | ||||||
|  |  | ||||||
|   Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler()))); |   Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler()))); | ||||||
|  |  | ||||||
|   return; |   return; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* | ||||||
|  |  * Construct a generic ListTile widget to link or un-link a custom barcode from a model. | ||||||
|  |  */ | ||||||
|  | Widget customBarcodeActionTile(BuildContext context, String barcode, String model, int pk) { | ||||||
|  |  | ||||||
|  |   if (barcode.isEmpty) { | ||||||
|  |     return ListTile( | ||||||
|  |       title: Text(L10().barcodeAssign), | ||||||
|  |       subtitle: Text(L10().barcodeAssignDetail), | ||||||
|  |       leading: Icon(Icons.qr_code, color: COLOR_CLICK), | ||||||
|  |       trailing: Icon(Icons.qr_code_scanner), | ||||||
|  |       onTap: () { | ||||||
|  |         var handler = UniqueBarcodeHandler((String barcode) { | ||||||
|  |           InvenTreeAPI().linkBarcode({ | ||||||
|  |             model: pk.toString(), | ||||||
|  |             "barcode": barcode, | ||||||
|  |           }).then((bool result) { | ||||||
|  |             showSnackIcon( | ||||||
|  |               result ? L10().barcodeAssigned : L10().barcodeNotAssigned, | ||||||
|  |               success: result | ||||||
|  |             ); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         Navigator.push( | ||||||
|  |           context, | ||||||
|  |           MaterialPageRoute( | ||||||
|  |             builder: (context) => InvenTreeQRView(handler) | ||||||
|  |           ) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   } else { | ||||||
|  |     return ListTile( | ||||||
|  |       title: Text(L10().barcodeUnassign), | ||||||
|  |       leading: Icon(Icons.qr_code, color: COLOR_CLICK), | ||||||
|  |       onTap: () async { | ||||||
|  |         InvenTreeAPI().unlinkBarcode({ | ||||||
|  |           model: pk.toString() | ||||||
|  |         }).then((bool result) { | ||||||
|  |           showSnackIcon( | ||||||
|  |             result ? L10().requestSuccessful : L10().requestFailed, | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
| } | } | ||||||
| @@ -17,7 +17,10 @@ List<String> debug_messages = []; | |||||||
|  |  | ||||||
| void clearDebugMessage() => debug_messages.clear(); | void clearDebugMessage() => debug_messages.clear(); | ||||||
|  |  | ||||||
| int debugMessageCount() => debug_messages.length; | int debugMessageCount() { | ||||||
|  |   print("Debug Messages: ${debug_messages.length}"); | ||||||
|  |   return debug_messages.length; | ||||||
|  | } | ||||||
|  |  | ||||||
| // Check if the debug log contains a given message | // Check if the debug log contains a given message | ||||||
| bool debugContains(String msg, {bool raiseAssert = true}) { | bool debugContains(String msg, {bool raiseAssert = true}) { | ||||||
|   | |||||||
| @@ -148,6 +148,23 @@ class InvenTreeModel { | |||||||
|   // Legacy API provided external link as "URL", while newer API uses "link" |   // Legacy API provided external link as "URL", while newer API uses "link" | ||||||
|   String get link => (jsondata["link"] ?? jsondata["URL"] ?? "") as String; |   String get link => (jsondata["link"] ?? jsondata["URL"] ?? "") as String; | ||||||
|  |  | ||||||
|  |   /* Extract any custom barcode data available for the model. | ||||||
|  |    * Note that old API used 'uid' (only for StockItem), | ||||||
|  |    * but this was updated to use 'barcode_hash' | ||||||
|  |    */ | ||||||
|  |   String get customBarcode { | ||||||
|  |     if (jsondata.containsKey("uid")) { | ||||||
|  |       return jsondata["uid"] as String; | ||||||
|  |     } else if (jsondata.containsKey("barcode_hash")) { | ||||||
|  |       return jsondata["barcode_hash"] as String; | ||||||
|  |     } else if (jsondata.containsKey("barcode")) { | ||||||
|  |       return jsondata["barcode"] as String; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Empty string if no match | ||||||
|  |     return ""; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future <void> goToInvenTreePage() async { |   Future <void> goToInvenTreePage() async { | ||||||
|  |  | ||||||
|     if (await canLaunch(webUrl)) { |     if (await canLaunch(webUrl)) { | ||||||
|   | |||||||
| @@ -276,8 +276,6 @@ class InvenTreeStockItem extends InvenTreeModel { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   String get uid => (jsondata["uid"] ?? "") as String; |  | ||||||
|  |  | ||||||
|   int get status => (jsondata["status"] ?? -1) as int; |   int get status => (jsondata["status"] ?? -1) as int; | ||||||
|  |  | ||||||
|   String get packaging => (jsondata["packaging"] ?? "") as String; |   String get packaging => (jsondata["packaging"] ?? "") as String; | ||||||
|   | |||||||
| @@ -807,6 +807,9 @@ | |||||||
|   "request": "Request", |   "request": "Request", | ||||||
|   "@request": {}, |   "@request": {}, | ||||||
|  |  | ||||||
|  |   "requestFailed": "Request Failed", | ||||||
|  |   "@requestFailed": {}, | ||||||
|  |  | ||||||
|   "requestSuccessful": "Request successful", |   "requestSuccessful": "Request successful", | ||||||
|   "@requestSuccessful": {}, |   "@requestSuccessful": {}, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -453,6 +453,12 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | |||||||
|             ) |             ) | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (InvenTreeAPI().supportModernBarcodes) { | ||||||
|  |           tiles.add( | ||||||
|  |             customBarcodeActionTile(context, location!.customBarcode, "stocklocation", location!.pk) | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; | |||||||
|  |  | ||||||
| import "package:inventree/api.dart"; | import "package:inventree/api.dart"; | ||||||
| import "package:inventree/app_colors.dart"; | import "package:inventree/app_colors.dart"; | ||||||
|  | import "package:inventree/barcode.dart"; | ||||||
| import "package:inventree/l10.dart"; | import "package:inventree/l10.dart"; | ||||||
| import "package:inventree/helpers.dart"; | import "package:inventree/helpers.dart"; | ||||||
|  |  | ||||||
| @@ -695,35 +696,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | |||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     // TODO - Add this action back in once implemented |     if (InvenTreeAPI().supportModernBarcodes) { | ||||||
|     /* |  | ||||||
|     tiles.add( |  | ||||||
|       ListTile( |  | ||||||
|         title: Text(L10().barcodeScanItem), |  | ||||||
|         leading: FaIcon(FontAwesomeIcons.box), |  | ||||||
|         trailing: Icon(Icons.qr_code), |  | ||||||
|         onTap: () { |  | ||||||
|           // TODO |  | ||||||
|         }, |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|     */ |  | ||||||
|  |  | ||||||
|     /* |  | ||||||
|     // TODO: Implement part deletion |  | ||||||
|     if (!part.isActive && InvenTreeAPI().checkPermission("part", "delete")) { |  | ||||||
|       tiles.add( |       tiles.add( | ||||||
|         ListTile( |         customBarcodeActionTile(context, part.customBarcode, "part", part.pk) | ||||||
|           title: Text(L10().deletePart), |  | ||||||
|           subtitle: Text(L10().deletePartDetail), |  | ||||||
|           leading: FaIcon(FontAwesomeIcons.trashAlt, color: COLOR_DANGER), |  | ||||||
|           onTap: () { |  | ||||||
|             // TODO |  | ||||||
|           }, |  | ||||||
|         ) |  | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|      */ |  | ||||||
|  |  | ||||||
|     return tiles; |     return tiles; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -416,48 +416,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   /* |  | ||||||
|    * Unassign (remove) a barcode from a StockItem. |  | ||||||
|    * |  | ||||||
|    * Note that for API version < 76 this action is performed on the StockItem endpoint. |  | ||||||
|    * For API version 76 or above, this uses the barcode "unlink" endpoint |  | ||||||
|    */ |  | ||||||
|   Future<void> _unassignBarcode(BuildContext context) async { |  | ||||||
|  |  | ||||||
|     if (InvenTreeAPI().apiVersion < 76) { |  | ||||||
|       final response = await item.update(values: {"uid": ""}); |  | ||||||
|  |  | ||||||
|       switch (response.statusCode) { |  | ||||||
|         case 200: |  | ||||||
|         case 201: |  | ||||||
|           showSnackIcon( |  | ||||||
|               L10().stockItemUpdateSuccess, |  | ||||||
|               success: true |  | ||||||
|           ); |  | ||||||
|           break; |  | ||||||
|         default: |  | ||||||
|           showSnackIcon( |  | ||||||
|             L10().stockItemUpdateFailure, |  | ||||||
|             success: false, |  | ||||||
|           ); |  | ||||||
|           break; |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       final bool result = await InvenTreeAPI().unlinkBarcode({ |  | ||||||
|         "stockitem": item.pk, |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       showSnackIcon( |  | ||||||
|         result ? L10().stockItemUpdateSuccess : L10().stockItemUpdateFailure, |  | ||||||
|         success: result, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     refresh(context); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * Launches an API Form to transfer this stock item to a new location |    * Launches an API Form to transfer this stock item to a new location | ||||||
|    */ |    */ | ||||||
| @@ -844,59 +802,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     // Add or remove custom barcode |     if (InvenTreeAPI().supportModernBarcodes) { | ||||||
|     if (item.uid.isEmpty) { |       tiles.add(customBarcodeActionTile(context, item.customBarcode, "stockitem", item.pk)); | ||||||
|       tiles.add( |  | ||||||
|         ListTile( |  | ||||||
|           title: Text(L10().barcodeAssign), |  | ||||||
|           subtitle: Text(L10().barcodeAssignDetail), |  | ||||||
|           leading: Icon(Icons.qr_code), |  | ||||||
|           trailing: Icon(Icons.qr_code_scanner), |  | ||||||
|           onTap: () { |  | ||||||
|  |  | ||||||
|             var handler = UniqueBarcodeHandler((String hash) { |  | ||||||
|               item.update( |  | ||||||
|                 values: { |  | ||||||
|                   "uid": hash, |  | ||||||
|                 } |  | ||||||
|               ).then((response) { |  | ||||||
|  |  | ||||||
|                 switch (response.statusCode) { |  | ||||||
|                   case 200: |  | ||||||
|                   case 201: |  | ||||||
|                     barcodeSuccessTone(); |  | ||||||
|  |  | ||||||
|                     showSnackIcon( |  | ||||||
|                       L10().barcodeAssigned, |  | ||||||
|                       success: true, |  | ||||||
|                       icon: Icons.qr_code, |  | ||||||
|                     ); |  | ||||||
|  |  | ||||||
|                     refresh(context); |  | ||||||
|                     break; |  | ||||||
|                   default: |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|               }); |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             Navigator.push( |  | ||||||
|               context, |  | ||||||
|               MaterialPageRoute(builder: (context) => InvenTreeQRView(handler)) |  | ||||||
|             ); |  | ||||||
|           } |  | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       tiles.add( |  | ||||||
|         ListTile( |  | ||||||
|           title: Text(L10().barcodeUnassign), |  | ||||||
|           leading: Icon(Icons.qr_code, color: COLOR_CLICK), |  | ||||||
|           onTap: () { |  | ||||||
|             _unassignBarcode(context); |  | ||||||
|           } |  | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Print label (if label printing plugins exist) |     // Print label (if label printing plugins exist) | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ import "package:inventree/barcode.dart"; | |||||||
| import "package:inventree/helpers.dart"; | import "package:inventree/helpers.dart"; | ||||||
| import "package:inventree/user_profile.dart"; | import "package:inventree/user_profile.dart"; | ||||||
|  |  | ||||||
|  | import "package:inventree/inventree/part.dart"; | ||||||
| import "package:inventree/inventree/stock.dart"; | import "package:inventree/inventree/stock.dart"; | ||||||
|  |  | ||||||
| void main() { | void main() { | ||||||
| @@ -75,7 +76,7 @@ void main() { | |||||||
|  |  | ||||||
|       debugContains("Scanned barcode data: '{\"stocklocation\": 999999}'"); |       debugContains("Scanned barcode data: '{\"stocklocation\": 999999}'"); | ||||||
|       debugContains("showSnackIcon: 'No match for barcode'"); |       debugContains("showSnackIcon: 'No match for barcode'"); | ||||||
|       assert(debugMessageCount() == 2); |       assert(debugMessageCount() == 3); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   }); |   }); | ||||||
| @@ -157,4 +158,42 @@ void main() { | |||||||
|       debugContains("showSnackIcon: 'Scanned into location'"); |       debugContains("showSnackIcon: 'Scanned into location'"); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   group("Test PartBarcodes:", () { | ||||||
|  |  | ||||||
|  |     // Assign a custom barcode to a Part instance | ||||||
|  |     test("Assign Barcode", () async { | ||||||
|  |  | ||||||
|  |       // Unlink barcode first | ||||||
|  |       await InvenTreeAPI().unlinkBarcode({ | ||||||
|  |         "part": "2" | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       final part = await InvenTreePart().get(2) as InvenTreePart?; | ||||||
|  |  | ||||||
|  |       assert(part != null); | ||||||
|  |       assert(part!.pk == 2); | ||||||
|  |  | ||||||
|  |       // Should have a "null" barcode | ||||||
|  |       assert(part!.customBarcode.isEmpty); | ||||||
|  |  | ||||||
|  |       // Assign custom barcode data to the part | ||||||
|  |       await InvenTreeAPI().linkBarcode({ | ||||||
|  |         "part": "2", | ||||||
|  |         "barcode": "xyz-123" | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       await part!.reload(); | ||||||
|  |       assert(part.customBarcode.isNotEmpty); | ||||||
|  |  | ||||||
|  |       // Check we can de-register a barcode also | ||||||
|  |       // Unlink barcode first | ||||||
|  |       await InvenTreeAPI().unlinkBarcode({ | ||||||
|  |         "part": "2" | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       await part.reload(); | ||||||
|  |       assert(part.customBarcode.isEmpty); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user