2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-28 05:26:47 +00:00

Merge pull request #140 from inventree/legacy-api

Remove support for legacy stock transfer API code

(cherry picked from commit 333e5bb41d24466077d3f1c2de15cc03baab276e)
This commit is contained in:
Oliver 2022-05-22 16:26:57 +10:00 committed by Oliver Walters
parent d796cd208d
commit bf722d6b76
6 changed files with 189 additions and 365 deletions

View File

@ -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
--- ---

View File

@ -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:
* *

View File

@ -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(

View File

@ -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,46 +531,29 @@ class InvenTreeStockItem extends InvenTreeModel {
Map<String, dynamic> data = {}; Map<String, dynamic> data = {};
// Note: Format of adjustment API was updated in API v14 data = {
if (api.supportsModernStockTransactions) { "items": [
// Modern (> 14) API {
data = {
"items": [
{
"pk": "${pk}",
"quantity": "${quantity}",
}
],
};
} else {
// Legacy (<= 14) API
data = {
"item": {
"pk": "${pk}", "pk": "${pk}",
"quantity": "${quantity}", "quantity": "${quantity}",
}, }
}; ],
} "notes": notes ?? "",
};
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;

View File

@ -1,15 +1,12 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:dropdown_search/dropdown_search.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/barcode.dart"; import "package:inventree/barcode.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/stock.dart";
import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/part.dart";
import "package:inventree/widget/dialogs.dart"; import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/fields.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/progress.dart"; import "package:inventree/widget/progress.dart";
@ -42,14 +39,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
@override @override
String getAppBarTitle(BuildContext context) => L10().stockItem; String getAppBarTitle(BuildContext context) => L10().stockItem;
final TextEditingController _quantityController = TextEditingController();
final TextEditingController _notesController = TextEditingController();
final _addStockKey = GlobalKey<FormState>();
final _removeStockKey = GlobalKey<FormState>();
final _countStockKey = GlobalKey<FormState>();
final _moveStockKey = GlobalKey<FormState>();
bool stockShowHistory = false; bool stockShowHistory = false;
@override @override
@ -295,76 +284,37 @@ 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 Map<String, dynamic> fields = {
if (InvenTreeAPI().supportsModernStockTransactions) { "pk": {
"parent": "items",
Map<String, dynamic> fields = { "nested": true,
"pk": { "hidden": true,
"parent": "items", "value": item.pk,
"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();
}, },
fields: <Widget> [ "quantity": {
Text("Current stock: ${item.quantity}"), "parent": "items",
QuantityField( "nested": true,
label: L10().addStock, "value": 0,
controller: _quantityController, },
), "notes": {},
TextFormField( };
decoration: InputDecoration(
labelText: L10().notes, launchApiForm(
), context,
controller: _notesController, L10().addStock,
) InvenTreeStockItem.addStockUrl(),
], fields,
method: "POST",
icon: FontAwesomeIcons.plusCircle,
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
}
); );
} }
@ -375,149 +325,68 @@ 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 Map<String, dynamic> fields = {
if (InvenTreeAPI().supportsModernStockTransactions) { "pk": {
Map<String, dynamic> fields = { "parent": "items",
"pk": { "nested": true,
"parent": "items", "hidden": true,
"nested": true, "value": item.pk,
"hidden": true, },
"value": item.pk, "quantity": {
}, "parent": "items",
"quantity": { "nested": true,
"parent": "items", "value": 0,
"nested": true, },
"value": 0, "notes": {},
}, };
"notes": {},
};
launchApiForm( launchApiForm(
context, context,
L10().removeStock, L10().removeStock,
InvenTreeStockItem.removeStockUrl(), InvenTreeStockItem.removeStockUrl(),
fields, fields,
method: "POST", method: "POST",
icon: FontAwesomeIcons.minusCircle, icon: FontAwesomeIcons.minusCircle,
onSuccess: (data) async { onSuccess: (data) async {
_stockUpdateMessage(true); _stockUpdateMessage(true);
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 Map<String, dynamic> fields = {
if (InvenTreeAPI().supportsModernStockTransactions) { "pk": {
"parent": "items",
Map<String, dynamic> fields = { "nested": true,
"pk": { "hidden": true,
"parent": "items", "value": item.pk,
"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();
}, },
acceptText: L10().count, "quantity": {
fields: <Widget> [ "parent": "items",
QuantityField( "nested": true,
label: L10().countStock, "value": item.quantity,
hint: "${item.quantityString}", },
controller: _quantityController, "notes": {},
), };
TextFormField(
decoration: InputDecoration( launchApiForm(
labelText: L10().notes, context,
), L10().countStock,
controller: _notesController, InvenTreeStockItem.countStockUrl(),
) fields,
] method: "POST",
icon: FontAwesomeIcons.clipboardCheck,
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
}
); );
} }
@ -542,130 +411,43 @@ 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 Map<String, dynamic> fields = {
if (InvenTreeAPI().supportsModernStockTransactions) { "pk": {
"parent": "items",
"nested": true,
"hidden": true,
"value": item.pk,
},
"quantity": {
"parent": "items",
"nested": true,
"value": item.quantity,
},
"location": {},
"notes": {},
};
Map<String, dynamic> fields = { if (item.isSerialized()) {
"pk": { // Prevent editing of 'quantity' field if the item is serialized
"parent": "items", fields["quantity"]["hidden"] = true;
"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;
} }
int? location_pk; launchApiForm(
context,
_quantityController.text = "${item.quantity}"; L10().transferStock,
InvenTreeStockItem.transferStockUrl(),
showFormDialog(L10().transferStock, fields,
key: _moveStockKey, method: "POST",
callback: () { icon: FontAwesomeIcons.dolly,
var _pk = location_pk; onSuccess: (data) async {
_stockUpdateMessage(true);
if (_pk != null) { refresh(context);
_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,
),
],
); );
} }

View File

@ -127,7 +127,6 @@ void main() {
assert(api.apiVersion >= 50); assert(api.apiVersion >= 50);
assert(api.supportsSettings); assert(api.supportsSettings);
assert(api.supportsNotifications); assert(api.supportsNotifications);
assert(api.supportsModernStockTransactions);
assert(api.supportsPoReceive); assert(api.supportsPoReceive);
// Check available permissions // Check available permissions