mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-30 21:05:42 +00:00 
			
		
		
		
	Remove support for legacy stock transfer API code
- Relies on modern API now - Checks for error messages against hidden fields in stock items
This commit is contained in:
		| @@ -5,6 +5,9 @@ | |||||||
| --- | --- | ||||||
|  |  | ||||||
| - Fixes issue which prevented text input in search window | - Fixes issue which prevented text input in search window | ||||||
|  | - Remove support for legacy stock adjustment API | ||||||
|  | - App now requires server API version 20 (or newer) | ||||||
|  | - Updated translation files | ||||||
|  |  | ||||||
| ### 0.7.0 - May 2022 | ### 0.7.0 - May 2022 | ||||||
| --- | --- | ||||||
|   | |||||||
| @@ -144,7 +144,7 @@ class InvenTreeAPI { | |||||||
|   InvenTreeAPI._internal(); |   InvenTreeAPI._internal(); | ||||||
|  |  | ||||||
|   // Minimum required API version for server |   // Minimum required API version for server | ||||||
|   static const _minApiVersion = 7; |   static const _minApiVersion = 20; | ||||||
|  |  | ||||||
|   bool _strictHttps = false; |   bool _strictHttps = false; | ||||||
|  |  | ||||||
| @@ -294,9 +294,6 @@ class InvenTreeAPI { | |||||||
|   // API endpoint for receiving purchase order line items was introduced in v12 |   // API endpoint for receiving purchase order line items was introduced in v12 | ||||||
|   bool get supportsPoReceive => apiVersion >= 12; |   bool get supportsPoReceive => apiVersion >= 12; | ||||||
|  |  | ||||||
|   // "Modern" API transactions were implemented in API v14 |  | ||||||
|   bool get supportsModernStockTransactions => apiVersion >= 14; |  | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * Connect to the remote InvenTree server: |    * Connect to the remote InvenTree server: | ||||||
|    * |    * | ||||||
|   | |||||||
| @@ -1161,6 +1161,72 @@ class _APIFormWidgetState extends State<APIFormWidget> { | |||||||
|     nonFieldErrors = errors; |     nonFieldErrors = errors; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /* Check for errors relating to an *unhandled* field name | ||||||
|  |   * These errors will not be displayed and potentially confuse the user | ||||||
|  |   * So, we need to know if these are ever happening | ||||||
|  |   */ | ||||||
|  |   void checkInvalidErrors(APIResponse response) { | ||||||
|  |     var errors = response.asMap(); | ||||||
|  |  | ||||||
|  |     for (String fieldName in errors.keys) { | ||||||
|  |  | ||||||
|  |       bool match = false; | ||||||
|  |  | ||||||
|  |       switch (fieldName) { | ||||||
|  |         case "__all__": | ||||||
|  |         case "non_field_errors": | ||||||
|  |         case "errors": | ||||||
|  |           // ignore these global fields | ||||||
|  |           match = true; | ||||||
|  |           continue; | ||||||
|  |         default: | ||||||
|  |           for (var field in fields) { | ||||||
|  |  | ||||||
|  |             // Hidden fields can't display errors, so we won't match | ||||||
|  |             if (field.hidden) { | ||||||
|  |               continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (field.name == fieldName) { | ||||||
|  |               // Direct Match found! | ||||||
|  |               match = true; | ||||||
|  |               break; | ||||||
|  |             } else if (field.parent == fieldName) { | ||||||
|  |  | ||||||
|  |               var error = errors[fieldName]; | ||||||
|  |  | ||||||
|  |               if (error is List) { | ||||||
|  |                 for (var el in error) { | ||||||
|  |                   if (el is Map && el.containsKey(field.name)) { | ||||||
|  |                     match = true; | ||||||
|  |                     break; | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } else if (error is Map && error.containsKey(field.name)) { | ||||||
|  |                 match = true; | ||||||
|  |                 break; | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           break; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!match) { | ||||||
|  |         // Match for an unknown / unsupported field | ||||||
|  |         sentryReportMessage( | ||||||
|  |           "API form returned error for unsupported field", | ||||||
|  |           context: { | ||||||
|  |             "url": response.url, | ||||||
|  |             "status_code": response.statusCode.toString(), | ||||||
|  |             "field": fieldName, | ||||||
|  |             "error_message": response.data.toString(), | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * Submit the form data to the server, and handle the results |    * Submit the form data to the server, and handle the results | ||||||
|    */ |    */ | ||||||
| @@ -1234,8 +1300,6 @@ class _APIFormWidgetState extends State<APIFormWidget> { | |||||||
|         // Hide this form |         // Hide this form | ||||||
|         Navigator.pop(context); |         Navigator.pop(context); | ||||||
|  |  | ||||||
|         // TODO: Display a snackBar |  | ||||||
|  |  | ||||||
|         if (successFunc != null) { |         if (successFunc != null) { | ||||||
|  |  | ||||||
|           // Ensure the response is a valid JSON structure |           // Ensure the response is a valid JSON structure | ||||||
| @@ -1263,7 +1327,7 @@ class _APIFormWidgetState extends State<APIFormWidget> { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         extractNonFieldErrors(response); |         extractNonFieldErrors(response); | ||||||
|  |         checkInvalidErrors(response); | ||||||
|         break; |         break; | ||||||
|       case 401: |       case 401: | ||||||
|         showSnackIcon( |         showSnackIcon( | ||||||
|   | |||||||
| @@ -517,7 +517,6 @@ class InvenTreeStockItem extends InvenTreeModel { | |||||||
|    * - Remove |    * - Remove | ||||||
|    * - Count |    * - Count | ||||||
|    */ |    */ | ||||||
|   // TODO: Remove this function when we deprecate support for the old API |  | ||||||
|   Future<bool> adjustStock(BuildContext context, String endpoint, double q, {String? notes, int? location}) async { |   Future<bool> adjustStock(BuildContext context, String endpoint, double q, {String? notes, int? location}) async { | ||||||
|  |  | ||||||
|     // Serialized stock cannot be adjusted (unless it is a "transfer") |     // Serialized stock cannot be adjusted (unless it is a "transfer") | ||||||
| @@ -532,9 +531,6 @@ class InvenTreeStockItem extends InvenTreeModel { | |||||||
|  |  | ||||||
|     Map<String, dynamic> data = {}; |     Map<String, dynamic> data = {}; | ||||||
|  |  | ||||||
|     // Note: Format of adjustment API was updated in API v14 |  | ||||||
|     if (api.supportsModernStockTransactions) { |  | ||||||
|       // Modern (> 14) API |  | ||||||
|     data = { |     data = { | ||||||
|       "items": [ |       "items": [ | ||||||
|         { |         { | ||||||
| @@ -542,36 +538,22 @@ class InvenTreeStockItem extends InvenTreeModel { | |||||||
|           "quantity": "${quantity}", |           "quantity": "${quantity}", | ||||||
|         } |         } | ||||||
|       ], |       ], | ||||||
|  |       "notes": notes ?? "", | ||||||
|     }; |     }; | ||||||
|     } else { |  | ||||||
|       // Legacy (<= 14) API |  | ||||||
|       data = { |  | ||||||
|         "item": { |  | ||||||
|           "pk": "${pk}", |  | ||||||
|           "quantity": "${quantity}", |  | ||||||
|         }, |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     data["notes"] = notes ?? ""; |  | ||||||
|  |  | ||||||
|     if (location != null) { |     if (location != null) { | ||||||
|       data["location"] = location; |       data["location"] = location; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Expected API return code depends on server API version |  | ||||||
|     final int expected_response = api.supportsModernStockTransactions ? 201 : 200; |  | ||||||
|  |  | ||||||
|     var response = await api.post( |     var response = await api.post( | ||||||
|       endpoint, |       endpoint, | ||||||
|       body: data, |       body: data, | ||||||
|       expectedStatusCode: expected_response, |       expectedStatusCode: 200, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return response.isValid(); |     return response.isValid(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // TODO: Remove this function when we deprecate support for the old API |  | ||||||
|   Future<bool> countStock(BuildContext context, double q, {String? notes}) async { |   Future<bool> countStock(BuildContext context, double q, {String? notes}) async { | ||||||
|  |  | ||||||
|     final bool result = await adjustStock(context, "/stock/count/", q, notes: notes); |     final bool result = await adjustStock(context, "/stock/count/", q, notes: notes); | ||||||
| @@ -579,7 +561,6 @@ class InvenTreeStockItem extends InvenTreeModel { | |||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // TODO: Remove this function when we deprecate support for the old API |  | ||||||
|   Future<bool> addStock(BuildContext context, double q, {String? notes}) async { |   Future<bool> addStock(BuildContext context, double q, {String? notes}) async { | ||||||
|  |  | ||||||
|     final bool result = await adjustStock(context,  "/stock/add/", q, notes: notes); |     final bool result = await adjustStock(context,  "/stock/add/", q, notes: notes); | ||||||
| @@ -587,7 +568,6 @@ class InvenTreeStockItem extends InvenTreeModel { | |||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // TODO: Remove this function when we deprecate support for the old API |  | ||||||
|   Future<bool> removeStock(BuildContext context, double q, {String? notes}) async { |   Future<bool> removeStock(BuildContext context, double q, {String? notes}) async { | ||||||
|  |  | ||||||
|     final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes); |     final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes); | ||||||
| @@ -595,7 +575,6 @@ class InvenTreeStockItem extends InvenTreeModel { | |||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // TODO: Remove this function when we deprecate support for the old API |  | ||||||
|   Future<bool> transferStock(BuildContext context, int location, {double? quantity, String? notes}) async { |   Future<bool> transferStock(BuildContext context, int location, {double? quantity, String? notes}) async { | ||||||
|  |  | ||||||
|     double q = this.quantity; |     double q = this.quantity; | ||||||
|   | |||||||
| @@ -295,24 +295,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future <void> _addStock() async { |   /* | ||||||
|  |    * Launch a dialog to 'add' quantity to this StockItem | ||||||
|     double quantity = double.parse(_quantityController.text); |    */ | ||||||
|     _quantityController.clear(); |  | ||||||
|  |  | ||||||
|     final bool result = await item.addStock(context, quantity, notes: _notesController.text); |  | ||||||
|     _notesController.clear(); |  | ||||||
|  |  | ||||||
|     _stockUpdateMessage(result); |  | ||||||
|  |  | ||||||
|     refresh(context); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future <void> _addStockDialog() async { |   Future <void> _addStockDialog() async { | ||||||
|  |  | ||||||
|     // TODO: In future, deprecate support for older API |  | ||||||
|     if (InvenTreeAPI().supportsModernStockTransactions) { |  | ||||||
|  |  | ||||||
|     Map<String, dynamic> fields = { |     Map<String, dynamic> fields = { | ||||||
|       "pk": { |       "pk": { | ||||||
|         "parent": "items", |         "parent": "items", | ||||||
| @@ -340,32 +327,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|         refresh(context); |         refresh(context); | ||||||
|       } |       } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     _quantityController.clear(); |  | ||||||
|     _notesController.clear(); |  | ||||||
|  |  | ||||||
|     showFormDialog( L10().addStock, |  | ||||||
|       key: _addStockKey, |  | ||||||
|       callback: () { |  | ||||||
|         _addStock(); |  | ||||||
|       }, |  | ||||||
|       fields: <Widget> [ |  | ||||||
|         Text("Current stock: ${item.quantity}"), |  | ||||||
|         QuantityField( |  | ||||||
|           label: L10().addStock, |  | ||||||
|           controller: _quantityController, |  | ||||||
|         ), |  | ||||||
|         TextFormField( |  | ||||||
|           decoration: InputDecoration( |  | ||||||
|             labelText: L10().notes, |  | ||||||
|           ), |  | ||||||
|           controller: _notesController, |  | ||||||
|         ) |  | ||||||
|       ], |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _stockUpdateMessage(bool result) { |   void _stockUpdateMessage(bool result) { | ||||||
| @@ -375,23 +336,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future <void> _removeStock() async { |   /* | ||||||
|  |    * Launch a dialog to 'remove' quantity from this StockItem | ||||||
|     double quantity = double.parse(_quantityController.text); |    */ | ||||||
|     _quantityController.clear(); |  | ||||||
|  |  | ||||||
|     final bool result = await item.removeStock(context, quantity, notes: _notesController.text); |  | ||||||
|  |  | ||||||
|     _stockUpdateMessage(result); |  | ||||||
|  |  | ||||||
|     refresh(context); |  | ||||||
|  |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _removeStockDialog() { |   void _removeStockDialog() { | ||||||
|  |  | ||||||
|     // TODO: In future, deprecate support for the older API |  | ||||||
|     if (InvenTreeAPI().supportsModernStockTransactions) { |  | ||||||
|     Map<String, dynamic> fields = { |     Map<String, dynamic> fields = { | ||||||
|       "pk": { |       "pk": { | ||||||
|         "parent": "items", |         "parent": "items", | ||||||
| @@ -419,52 +368,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|           refresh(context); |           refresh(context); | ||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     _quantityController.clear(); |  | ||||||
|     _notesController.clear(); |  | ||||||
|  |  | ||||||
|     showFormDialog(L10().removeStock, |  | ||||||
|         key: _removeStockKey, |  | ||||||
|         callback: () { |  | ||||||
|           _removeStock(); |  | ||||||
|         }, |  | ||||||
|         fields: <Widget>[ |  | ||||||
|           Text("Current stock: ${item.quantity}"), |  | ||||||
|           QuantityField( |  | ||||||
|             label: L10().removeStock, |  | ||||||
|             controller: _quantityController, |  | ||||||
|             max: item.quantity, |  | ||||||
|           ), |  | ||||||
|           TextFormField( |  | ||||||
|             decoration: InputDecoration( |  | ||||||
|               labelText: L10().notes, |  | ||||||
|             ), |  | ||||||
|             controller: _notesController, |  | ||||||
|           ), |  | ||||||
|         ], |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future <void> _countStock() async { |  | ||||||
|  |  | ||||||
|     double quantity = double.parse(_quantityController.text); |  | ||||||
|     _quantityController.clear(); |  | ||||||
|  |  | ||||||
|     final bool result = await item.countStock(context, quantity, notes: _notesController.text); |  | ||||||
|  |  | ||||||
|     _stockUpdateMessage(result); |  | ||||||
|  |  | ||||||
|     refresh(context); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future <void> _countStockDialog() async { |   Future <void> _countStockDialog() async { | ||||||
|  |  | ||||||
|     // TODO: In future, deprecate support for older API |  | ||||||
|     if (InvenTreeAPI().supportsModernStockTransactions) { |  | ||||||
|  |  | ||||||
|     Map<String, dynamic> fields = { |     Map<String, dynamic> fields = { | ||||||
|       "pk": { |       "pk": { | ||||||
|         "parent": "items", |         "parent": "items", | ||||||
| @@ -492,33 +399,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|           refresh(context); |           refresh(context); | ||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     _quantityController.text = item.quantity.toString(); |  | ||||||
|     _notesController.clear(); |  | ||||||
|  |  | ||||||
|     showFormDialog(L10().countStock, |  | ||||||
|       key: _countStockKey, |  | ||||||
|       callback: () { |  | ||||||
|         _countStock(); |  | ||||||
|       }, |  | ||||||
|       acceptText: L10().count, |  | ||||||
|       fields: <Widget> [ |  | ||||||
|         QuantityField( |  | ||||||
|           label: L10().countStock, |  | ||||||
|           hint: "${item.quantityString}", |  | ||||||
|           controller: _quantityController, |  | ||||||
|         ), |  | ||||||
|         TextFormField( |  | ||||||
|           decoration: InputDecoration( |  | ||||||
|             labelText: L10().notes, |  | ||||||
|           ), |  | ||||||
|           controller: _notesController, |  | ||||||
|         ) |  | ||||||
|       ] |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -542,32 +422,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   // TODO: Delete this function once support for old API is deprecated |  | ||||||
|   Future <void> _transferStock(int locationId) async { |  | ||||||
|  |  | ||||||
|     double quantity = double.tryParse(_quantityController.text) ?? item.quantity; |  | ||||||
|     String notes = _notesController.text; |  | ||||||
|  |  | ||||||
|     _quantityController.clear(); |  | ||||||
|     _notesController.clear(); |  | ||||||
|  |  | ||||||
|     var result = await item.transferStock(context, locationId, quantity: quantity, notes: notes); |  | ||||||
|  |  | ||||||
|     refresh(context); |  | ||||||
|  |  | ||||||
|     if (result) { |  | ||||||
|       showSnackIcon(L10().stockItemTransferred, success: true); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * 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 | ||||||
|    */ |    */ | ||||||
|   Future <void> _transferStockDialog(BuildContext context) async { |   Future <void> _transferStockDialog(BuildContext context) async { | ||||||
|  |  | ||||||
|     // TODO: In future, deprecate support for older API |  | ||||||
|     if (InvenTreeAPI().supportsModernStockTransactions) { |  | ||||||
|  |  | ||||||
|     Map<String, dynamic> fields = { |     Map<String, dynamic> fields = { | ||||||
|       "pk": { |       "pk": { | ||||||
|         "parent": "items", |         "parent": "items", | ||||||
| @@ -584,6 +443,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|       "notes": {}, |       "notes": {}, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     if (item.isSerialized()) { | ||||||
|  |       // Prevent editing of 'quantity' field if the item is serialized | ||||||
|  |       fields["quantity"]["hidden"] = true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     launchApiForm( |     launchApiForm( | ||||||
|         context, |         context, | ||||||
|         L10().transferStock, |         L10().transferStock, | ||||||
| @@ -596,77 +460,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | |||||||
|           refresh(context); |           refresh(context); | ||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     int? location_pk; |  | ||||||
|  |  | ||||||
|     _quantityController.text = "${item.quantity}"; |  | ||||||
|  |  | ||||||
|     showFormDialog(L10().transferStock, |  | ||||||
|         key: _moveStockKey, |  | ||||||
|         callback: () { |  | ||||||
|           var _pk = location_pk; |  | ||||||
|  |  | ||||||
|           if (_pk != null) { |  | ||||||
|             _transferStock(_pk); |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         fields: <Widget>[ |  | ||||||
|           QuantityField( |  | ||||||
|             label: L10().quantity, |  | ||||||
|             controller: _quantityController, |  | ||||||
|             max: item.quantity, |  | ||||||
|           ), |  | ||||||
|           DropdownSearch<dynamic>( |  | ||||||
|             mode: Mode.BOTTOM_SHEET, |  | ||||||
|             showSelectedItem: false, |  | ||||||
|             autoFocusSearchBox: true, |  | ||||||
|             selectedItem: null, |  | ||||||
|             errorBuilder: (context, entry, exception) { |  | ||||||
|               print("entry: $entry"); |  | ||||||
|               print(exception.toString()); |  | ||||||
|  |  | ||||||
|               return Text( |  | ||||||
|                 exception.toString(), |  | ||||||
|                 style: TextStyle( |  | ||||||
|                   fontSize: 10, |  | ||||||
|                 ) |  | ||||||
|               ); |  | ||||||
|             }, |  | ||||||
|             onFind: (String filter) async { |  | ||||||
|  |  | ||||||
|               final results = await InvenTreeStockLocation().search(filter); |  | ||||||
|  |  | ||||||
|               List<dynamic> items = []; |  | ||||||
|  |  | ||||||
|               for (InvenTreeModel loc in results) { |  | ||||||
|                 if (loc is InvenTreeStockLocation) { |  | ||||||
|                   items.add(loc.jsondata); |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|  |  | ||||||
|               return items; |  | ||||||
|             }, |  | ||||||
|             label: L10().stockLocation, |  | ||||||
|             hint: L10().searchLocation, |  | ||||||
|             onChanged: null, |  | ||||||
|             itemAsString: (dynamic location) { |  | ||||||
|               return (location["pathstring"] ?? "") as String; |  | ||||||
|             }, |  | ||||||
|             onSaved: (dynamic location) { |  | ||||||
|               if (location == null) { |  | ||||||
|                 location_pk = null; |  | ||||||
|               } else { |  | ||||||
|                 location_pk = location["pk"] as int; |  | ||||||
|               } |  | ||||||
|             }, |  | ||||||
|             isFilteredOnline: true, |  | ||||||
|             showSearchBox:  true, |  | ||||||
|           ), |  | ||||||
|         ], |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget headerTile() { |   Widget headerTile() { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user