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:
parent
d796cd208d
commit
bf722d6b76
@ -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
|
||||
---
|
||||
|
@ -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:
|
||||
*
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user