2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-27 21:16:48 +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
- Remove support for legacy stock adjustment API
- App now requires server API version 20 (or newer)
- Updated translation files
### 0.7.0 - May 2022
---

View File

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

View File

@ -1161,6 +1161,72 @@ class _APIFormWidgetState extends State<APIFormWidget> {
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<APIFormWidget> {
// 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<APIFormWidget> {
}
extractNonFieldErrors(response);
checkInvalidErrors(response);
break;
case 401:
showSnackIcon(

View File

@ -517,7 +517,6 @@ class InvenTreeStockItem extends InvenTreeModel {
* - Remove
* - 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 {
// Serialized stock cannot be adjusted (unless it is a "transfer")
@ -532,46 +531,29 @@ class InvenTreeStockItem extends InvenTreeModel {
Map<String, dynamic> 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<bool> 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<bool> 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<bool> 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<bool> transferStock(BuildContext context, int location, {double? quantity, String? notes}) async {
double q = this.quantity;

View File

@ -1,15 +1,12 @@
import "package:flutter/material.dart";
import "package:dropdown_search/dropdown_search.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/barcode.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/fields.dart";
import "package:inventree/widget/location_display.dart";
import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/progress.dart";
@ -42,14 +39,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
@override
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;
@override
@ -295,76 +284,37 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
Future <void> _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 <void> _addStockDialog() async {
// TODO: In future, deprecate support for older API
if (InvenTreeAPI().supportsModernStockTransactions) {
Map<String, dynamic> 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<String, dynamic> fields = {
"pk": {
"parent": "items",
"nested": true,
"hidden": true,
"value": item.pk,
},
fields: <Widget> [
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 +325,68 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
}
Future <void> _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<String, dynamic> fields = {
"pk": {
"parent": "items",
"nested": true,
"hidden": true,
"value": item.pk,
},
"quantity": {
"parent": "items",
"nested": true,
"value": 0,
},
"notes": {},
};
Map<String, dynamic> 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: <Widget>[
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 <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 {
// TODO: In future, deprecate support for older API
if (InvenTreeAPI().supportsModernStockTransactions) {
Map<String, dynamic> 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<String, dynamic> fields = {
"pk": {
"parent": "items",
"nested": true,
"hidden": true,
"value": item.pk,
},
acceptText: L10().count,
fields: <Widget> [
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 +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
*/
Future <void> _transferStockDialog(BuildContext context) async {
// TODO: In future, deprecate support for older API
if (InvenTreeAPI().supportsModernStockTransactions) {
Map<String, dynamic> fields = {
"pk": {
"parent": "items",
"nested": true,
"hidden": true,
"value": item.pk,
},
"quantity": {
"parent": "items",
"nested": true,
"value": item.quantity,
},
"location": {},
"notes": {},
};
Map<String, dynamic> 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: <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,
),
],
launchApiForm(
context,
L10().transferStock,
InvenTreeStockItem.transferStockUrl(),
fields,
method: "POST",
icon: FontAwesomeIcons.dolly,
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
}
);
}

View File

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