diff --git a/assets/release_notes.md b/assets/release_notes.md index e240c26d..23c44dcc 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -5,6 +5,9 @@ --- - 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 --- diff --git a/lib/api.dart b/lib/api.dart index 52bd29a1..99b42424 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -144,7 +144,7 @@ class InvenTreeAPI { InvenTreeAPI._internal(); // Minimum required API version for server - static const _minApiVersion = 7; + static const _minApiVersion = 20; bool _strictHttps = false; @@ -294,9 +294,6 @@ class InvenTreeAPI { // API endpoint for receiving purchase order line items was introduced in v12 bool get supportsPoReceive => apiVersion >= 12; - // "Modern" API transactions were implemented in API v14 - bool get supportsModernStockTransactions => apiVersion >= 14; - /* * Connect to the remote InvenTree server: * diff --git a/lib/api_form.dart b/lib/api_form.dart index 588658bc..04a51dde 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -1161,6 +1161,72 @@ class _APIFormWidgetState extends State { 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 */ @@ -1234,8 +1300,6 @@ class _APIFormWidgetState extends State { // Hide this form Navigator.pop(context); - // TODO: Display a snackBar - if (successFunc != null) { // Ensure the response is a valid JSON structure @@ -1263,7 +1327,7 @@ class _APIFormWidgetState extends State { } extractNonFieldErrors(response); - + checkInvalidErrors(response); break; case 401: showSnackIcon( diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 2bb741f2..50b5ebe7 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -517,7 +517,6 @@ class InvenTreeStockItem extends InvenTreeModel { * - Remove * - Count */ - // TODO: Remove this function when we deprecate support for the old API Future adjustStock(BuildContext context, String endpoint, double q, {String? notes, int? location}) async { // Serialized stock cannot be adjusted (unless it is a "transfer") @@ -532,46 +531,29 @@ class InvenTreeStockItem extends InvenTreeModel { Map data = {}; - // Note: Format of adjustment API was updated in API v14 - if (api.supportsModernStockTransactions) { - // Modern (> 14) API - data = { - "items": [ - { - "pk": "${pk}", - "quantity": "${quantity}", - } - ], - }; - } else { - // Legacy (<= 14) API - data = { - "item": { + data = { + "items": [ + { "pk": "${pk}", "quantity": "${quantity}", - }, - }; - } - - data["notes"] = notes ?? ""; + } + ], + "notes": notes ?? "", + }; if (location != null) { 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( endpoint, body: data, - expectedStatusCode: expected_response, + expectedStatusCode: 200, ); return response.isValid(); } - // TODO: Remove this function when we deprecate support for the old API Future countStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/count/", q, notes: notes); @@ -579,7 +561,6 @@ class InvenTreeStockItem extends InvenTreeModel { return result; } - // TODO: Remove this function when we deprecate support for the old API Future addStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/add/", q, notes: notes); @@ -587,7 +568,6 @@ class InvenTreeStockItem extends InvenTreeModel { return result; } - // TODO: Remove this function when we deprecate support for the old API Future removeStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes); @@ -595,7 +575,6 @@ class InvenTreeStockItem extends InvenTreeModel { return result; } - // TODO: Remove this function when we deprecate support for the old API Future transferStock(BuildContext context, int location, {double? quantity, String? notes}) async { double q = this.quantity; diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index e4773755..ab59a700 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -295,76 +295,37 @@ class _StockItemDisplayState extends RefreshableState { } - Future _addStock() async { - - 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); - } - + /* + * Launch a dialog to 'add' quantity to this StockItem + */ Future _addStockDialog() async { - // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportsModernStockTransactions) { - - Map fields = { - "pk": { - "parent": "items", - "nested": true, - "hidden": true, - "value": item.pk, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": 0, - }, - "notes": {}, - }; - - launchApiForm( - context, - L10().addStock, - InvenTreeStockItem.addStockUrl(), - fields, - method: "POST", - icon: FontAwesomeIcons.plusCircle, - onSuccess: (data) async { - _stockUpdateMessage(true); - refresh(context); - } - ); - - return; - } - - _quantityController.clear(); - _notesController.clear(); - - showFormDialog( L10().addStock, - key: _addStockKey, - callback: () { - _addStock(); + Map fields = { + "pk": { + "parent": "items", + "nested": true, + "hidden": true, + "value": item.pk, }, - fields: [ - Text("Current stock: ${item.quantity}"), - QuantityField( - label: L10().addStock, - controller: _quantityController, - ), - TextFormField( - decoration: InputDecoration( - labelText: L10().notes, - ), - controller: _notesController, - ) - ], + "quantity": { + "parent": "items", + "nested": true, + "value": 0, + }, + "notes": {}, + }; + + launchApiForm( + context, + L10().addStock, + InvenTreeStockItem.addStockUrl(), + fields, + method: "POST", + icon: FontAwesomeIcons.plusCircle, + onSuccess: (data) async { + _stockUpdateMessage(true); + refresh(context); + } ); } @@ -375,149 +336,68 @@ class _StockItemDisplayState extends RefreshableState { } } - Future _removeStock() async { - - double quantity = double.parse(_quantityController.text); - _quantityController.clear(); - - final bool result = await item.removeStock(context, quantity, notes: _notesController.text); - - _stockUpdateMessage(result); - - refresh(context); - - } - + /* + * Launch a dialog to 'remove' quantity from this StockItem + */ void _removeStockDialog() { - // TODO: In future, deprecate support for the older API - if (InvenTreeAPI().supportsModernStockTransactions) { - Map fields = { - "pk": { - "parent": "items", - "nested": true, - "hidden": true, - "value": item.pk, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": 0, - }, - "notes": {}, - }; + Map fields = { + "pk": { + "parent": "items", + "nested": true, + "hidden": true, + "value": item.pk, + }, + "quantity": { + "parent": "items", + "nested": true, + "value": 0, + }, + "notes": {}, + }; - launchApiForm( - context, - L10().removeStock, - InvenTreeStockItem.removeStockUrl(), - fields, - method: "POST", - icon: FontAwesomeIcons.minusCircle, - onSuccess: (data) async { - _stockUpdateMessage(true); - refresh(context); - } - ); - - return; - } - - _quantityController.clear(); - _notesController.clear(); - - showFormDialog(L10().removeStock, - key: _removeStockKey, - callback: () { - _removeStock(); - }, - fields: [ - Text("Current stock: ${item.quantity}"), - QuantityField( - label: L10().removeStock, - controller: _quantityController, - max: item.quantity, - ), - TextFormField( - decoration: InputDecoration( - labelText: L10().notes, - ), - controller: _notesController, - ), - ], + launchApiForm( + context, + L10().removeStock, + InvenTreeStockItem.removeStockUrl(), + fields, + method: "POST", + icon: FontAwesomeIcons.minusCircle, + onSuccess: (data) async { + _stockUpdateMessage(true); + refresh(context); + } ); } - Future _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 _countStockDialog() async { - // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportsModernStockTransactions) { - - Map fields = { - "pk": { - "parent": "items", - "nested": true, - "hidden": true, - "value": item.pk, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": item.quantity, - }, - "notes": {}, - }; - - launchApiForm( - context, - L10().countStock, - InvenTreeStockItem.countStockUrl(), - fields, - method: "POST", - icon: FontAwesomeIcons.clipboardCheck, - onSuccess: (data) async { - _stockUpdateMessage(true); - refresh(context); - } - ); - - return; - } - - _quantityController.text = item.quantity.toString(); - _notesController.clear(); - - showFormDialog(L10().countStock, - key: _countStockKey, - callback: () { - _countStock(); + Map fields = { + "pk": { + "parent": "items", + "nested": true, + "hidden": true, + "value": item.pk, }, - acceptText: L10().count, - fields: [ - QuantityField( - label: L10().countStock, - hint: "${item.quantityString}", - controller: _quantityController, - ), - TextFormField( - decoration: InputDecoration( - labelText: L10().notes, - ), - controller: _notesController, - ) - ] + "quantity": { + "parent": "items", + "nested": true, + "value": item.quantity, + }, + "notes": {}, + }; + + launchApiForm( + context, + L10().countStock, + InvenTreeStockItem.countStockUrl(), + fields, + method: "POST", + icon: FontAwesomeIcons.clipboardCheck, + onSuccess: (data) async { + _stockUpdateMessage(true); + refresh(context); + } ); } @@ -542,130 +422,43 @@ class _StockItemDisplayState extends RefreshableState { } - // TODO: Delete this function once support for old API is deprecated - Future _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 */ Future _transferStockDialog(BuildContext context) async { - // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportsModernStockTransactions) { + Map fields = { + "pk": { + "parent": "items", + "nested": true, + "hidden": true, + "value": item.pk, + }, + "quantity": { + "parent": "items", + "nested": true, + "value": item.quantity, + }, + "location": {}, + "notes": {}, + }; - Map fields = { - "pk": { - "parent": "items", - "nested": true, - "hidden": true, - "value": item.pk, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": item.quantity, - }, - "location": {}, - "notes": {}, - }; - - launchApiForm( - context, - L10().transferStock, - InvenTreeStockItem.transferStockUrl(), - fields, - method: "POST", - icon: FontAwesomeIcons.dolly, - onSuccess: (data) async { - _stockUpdateMessage(true); - refresh(context); - } - ); - - return; + if (item.isSerialized()) { + // Prevent editing of 'quantity' field if the item is serialized + fields["quantity"]["hidden"] = true; } - int? location_pk; - - _quantityController.text = "${item.quantity}"; - - showFormDialog(L10().transferStock, - key: _moveStockKey, - callback: () { - var _pk = location_pk; - - if (_pk != null) { - _transferStock(_pk); - } - }, - fields: [ - QuantityField( - label: L10().quantity, - controller: _quantityController, - max: item.quantity, - ), - DropdownSearch( - 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 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, - ), - ], + launchApiForm( + context, + L10().transferStock, + InvenTreeStockItem.transferStockUrl(), + fields, + method: "POST", + icon: FontAwesomeIcons.dolly, + onSuccess: (data) async { + _stockUpdateMessage(true); + refresh(context); + } ); }