mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 05:26:47 +00:00
Stock location scan (#169)
* Add action for scanning a stock location into another location * Adds barcode scan handler for new functionality * Handle scanning of stock location * Cleanup * Refactor existing barcode scanning functions - Will require extensive testing and validation * Add entry to release notes * Delete dead code * Improved ordering based on stock quantity * Bug fix for 'adjustStock' function * Improve error responses for barcode scanning * Improve error responses for barcode scanning * Remove old debug statements * Add some extra explanatory texts * Icon change * Fixes for unit tests * Adds extra functionality for user profile manager * Refactor barcode code - do not rely on BuildContext * Adds initial unit testing for barcode scanning - Work on mocking barcode data - Add hooks for testing snackBar and audio files * Linting fixes * More barcode unit tests * Cleanup unit tests for barcode * Remove unused import * Handle HTTPException in API * Improvements for API unit testing * Unit testing for scanning item into location * Add unit test for scanning in items from a location context * Unit test for scanning location into parent location * Improve feedback for barcode scanning events
This commit is contained in:
parent
c6678e201f
commit
aa274b2e45
@ -8,6 +8,7 @@
|
|||||||
- Display Bill of Materials in the part detail view
|
- Display Bill of Materials in the part detail view
|
||||||
- Indicate available quantity in stock detail view
|
- Indicate available quantity in stock detail view
|
||||||
- Adds configurable filtering to various list views
|
- Adds configurable filtering to various list views
|
||||||
|
- Allow stock location to be "scanned" into another location using barcode
|
||||||
|
|
||||||
### 0.7.3 - June 2022
|
### 0.7.3 - June 2022
|
||||||
---
|
---
|
||||||
|
@ -413,6 +413,8 @@ class InvenTreeAPI {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug("Token request failed: STATUS ${response.statusCode}");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1017,14 +1019,16 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (statusCode != null) {
|
if (statusCode != null) {
|
||||||
|
|
||||||
// Expected status code not returned
|
// Expected status code not returned
|
||||||
if (statusCode != _response.statusCode) {
|
if (statusCode != _response.statusCode) {
|
||||||
showStatusCodeError(url, _response.statusCode);
|
showStatusCodeError(url, _response.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} on HttpException catch (error) {
|
||||||
|
showServerError(url, L10().serverError, error.toString());
|
||||||
|
response.error = "HTTPException";
|
||||||
|
response.errorDetail = error.toString();
|
||||||
} on SocketException catch (error) {
|
} on SocketException catch (error) {
|
||||||
showServerError(url, L10().connectionRefused, error.toString());
|
showServerError(url, L10().connectionRefused, error.toString());
|
||||||
response.error = "SocketException";
|
response.error = "SocketException";
|
||||||
|
@ -857,6 +857,7 @@ Future<void> launchApiForm(
|
|||||||
Map<String, dynamic> serverFields = {};
|
Map<String, dynamic> serverFields = {};
|
||||||
|
|
||||||
if (url.isNotEmpty) {
|
if (url.isNotEmpty) {
|
||||||
|
|
||||||
var options = await InvenTreeAPI().options(url);
|
var options = await InvenTreeAPI().options(url);
|
||||||
|
|
||||||
// Invalid response from server
|
// Invalid response from server
|
||||||
|
377
lib/barcode.dart
377
lib/barcode.dart
@ -43,14 +43,13 @@ Future <void> barcodeFailureTone() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BarcodeHandler {
|
/* Generic class which "handles" a barcode, by communicating with the InvenTree server,
|
||||||
/*
|
|
||||||
* Class which "handles" a barcode, by communicating with the InvenTree server,
|
|
||||||
* and handling match / unknown / error cases.
|
* and handling match / unknown / error cases.
|
||||||
*
|
*
|
||||||
* Override functionality of this class to perform custom actions,
|
* Override functionality of this class to perform custom actions,
|
||||||
* based on the response returned from the InvenTree server
|
* based on the response returned from the InvenTree server
|
||||||
*/
|
*/
|
||||||
|
class BarcodeHandler {
|
||||||
|
|
||||||
BarcodeHandler();
|
BarcodeHandler();
|
||||||
|
|
||||||
@ -58,12 +57,12 @@ class BarcodeHandler {
|
|||||||
|
|
||||||
QRViewController? _controller;
|
QRViewController? _controller;
|
||||||
|
|
||||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||||
// Called when the server "matches" a barcode
|
// Called when the server "matches" a barcode
|
||||||
// Override this function
|
// Override this function
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||||
// Called when the server does not know about a barcode
|
// Called when the server does not know about a barcode
|
||||||
// Override this function
|
// Override this function
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ class BarcodeHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onBarcodeUnhandled(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
|
||||||
|
|
||||||
barcodeFailureTone();
|
barcodeFailureTone();
|
||||||
|
|
||||||
@ -86,12 +85,27 @@ class BarcodeHandler {
|
|||||||
_controller?.resumeCamera();
|
_controller?.resumeCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> processBarcode(BuildContext context, QRViewController? _controller, String barcode, {String url = "barcode/"}) async {
|
/*
|
||||||
|
* Base function to capture and process barcode data.
|
||||||
|
*/
|
||||||
|
Future<void> processBarcode(QRViewController? _controller, String barcode, {String url = "barcode/"}) async {
|
||||||
this._controller = _controller;
|
this._controller = _controller;
|
||||||
|
|
||||||
print("Scanned barcode data: ${barcode}");
|
debug("Scanned barcode data: '${barcode}'");
|
||||||
|
|
||||||
|
barcode = barcode.trim();
|
||||||
|
|
||||||
|
// Empty barcode is invalid
|
||||||
if (barcode.isEmpty) {
|
if (barcode.isEmpty) {
|
||||||
|
|
||||||
|
barcodeFailureTone();
|
||||||
|
|
||||||
|
showSnackIcon(
|
||||||
|
L10().barcodeError,
|
||||||
|
icon: FontAwesomeIcons.exclamationCircle,
|
||||||
|
success: false
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +123,9 @@ class BarcodeHandler {
|
|||||||
|
|
||||||
// Handle strange response from the server
|
// Handle strange response from the server
|
||||||
if (!response.isValid() || !response.isMap()) {
|
if (!response.isValid() || !response.isMap()) {
|
||||||
onBarcodeUnknown(context, {});
|
onBarcodeUnknown({});
|
||||||
|
|
||||||
|
showSnackIcon(L10().serverError, success: false);
|
||||||
|
|
||||||
// We want to know about this one!
|
// We want to know about this one!
|
||||||
await sentryReportMessage(
|
await sentryReportMessage(
|
||||||
@ -122,31 +138,36 @@ class BarcodeHandler {
|
|||||||
"valid": response.isValid().toString(),
|
"valid": response.isValid().toString(),
|
||||||
"error": response.error,
|
"error": response.error,
|
||||||
"errorDetail": response.errorDetail,
|
"errorDetail": response.errorDetail,
|
||||||
"overlayText": getOverlayText(context),
|
"className": "${this}",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else if ((response.statusCode >= 400) || data.containsKey("error")) {
|
|
||||||
onBarcodeUnknown(context, data);
|
|
||||||
} else if (data.containsKey("success")) {
|
} else if (data.containsKey("success")) {
|
||||||
onBarcodeMatched(context, data);
|
await onBarcodeMatched(data);
|
||||||
|
} else if ((response.statusCode >= 400) || data.containsKey("error")) {
|
||||||
|
await onBarcodeUnknown(data);
|
||||||
} else {
|
} else {
|
||||||
onBarcodeUnhandled(context, data);
|
await onBarcodeUnhandled(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
class BarcodeScanHandler extends BarcodeHandler {
|
|
||||||
/*
|
|
||||||
* Class for general barcode scanning.
|
* Class for general barcode scanning.
|
||||||
* Scan *any* barcode without context, and then redirect app to correct view
|
* Scan *any* barcode without context, and then redirect app to correct view.
|
||||||
|
*
|
||||||
|
* Handles scanning of:
|
||||||
|
*
|
||||||
|
* - StockLocation
|
||||||
|
* - StockItem
|
||||||
|
* - Part
|
||||||
*/
|
*/
|
||||||
|
class BarcodeScanHandler extends BarcodeHandler {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getOverlayText(BuildContext context) => L10().barcodeScanGeneral;
|
String getOverlayText(BuildContext context) => L10().barcodeScanGeneral;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||||
|
|
||||||
barcodeFailureTone();
|
barcodeFailureTone();
|
||||||
|
|
||||||
@ -158,7 +179,7 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||||
|
|
||||||
int pk = -1;
|
int pk = -1;
|
||||||
|
|
||||||
@ -173,8 +194,13 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
|
|
||||||
InvenTreeStockLocation().get(pk).then((var loc) {
|
InvenTreeStockLocation().get(pk).then((var loc) {
|
||||||
if (loc is InvenTreeStockLocation) {
|
if (loc is InvenTreeStockLocation) {
|
||||||
Navigator.of(context).pop();
|
showSnackIcon(
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
|
L10().stockLocation,
|
||||||
|
success: true,
|
||||||
|
icon: Icons.qr_code,
|
||||||
|
);
|
||||||
|
OneContext().pop();
|
||||||
|
OneContext().navigator.push(MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -196,12 +222,14 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
barcodeSuccessTone();
|
barcodeSuccessTone();
|
||||||
|
|
||||||
InvenTreeStockItem().get(pk).then((var item) {
|
InvenTreeStockItem().get(pk).then((var item) {
|
||||||
|
showSnackIcon(
|
||||||
// Dispose of the barcode scanner
|
L10().stockItem,
|
||||||
Navigator.of(context).pop();
|
success: true,
|
||||||
|
icon: Icons.qr_code,
|
||||||
|
);
|
||||||
|
OneContext().pop();
|
||||||
if (item is InvenTreeStockItem) {
|
if (item is InvenTreeStockItem) {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
|
OneContext().push(MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -222,12 +250,16 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
barcodeSuccessTone();
|
barcodeSuccessTone();
|
||||||
|
|
||||||
InvenTreePart().get(pk).then((var part) {
|
InvenTreePart().get(pk).then((var part) {
|
||||||
|
showSnackIcon(
|
||||||
|
L10().part,
|
||||||
|
success: true,
|
||||||
|
icon: Icons.qr_code,
|
||||||
|
);
|
||||||
// Dismiss the barcode scanner
|
// Dismiss the barcode scanner
|
||||||
Navigator.of(context).pop();
|
OneContext().pop();
|
||||||
|
|
||||||
if (part is InvenTreePart) {
|
if (part is InvenTreePart) {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
OneContext().push(MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -265,73 +297,148 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StockItemScanIntoLocationHandler extends BarcodeHandler {
|
|
||||||
/*
|
/*
|
||||||
* Barcode handler for scanning a provided StockItem into a scanned StockLocation
|
* Generic class for scanning a StockLocation.
|
||||||
|
*
|
||||||
|
* - Validates that the scanned barcode matches a valid StockLocation
|
||||||
|
* - Runs a "callback" function if a valid StockLocation is found
|
||||||
*/
|
*/
|
||||||
|
class BarcodeScanStockLocationHandler extends BarcodeHandler {
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||||
|
|
||||||
|
// We expect that the barcode points to a 'stocklocation'
|
||||||
|
if (data.containsKey("stocklocation")) {
|
||||||
|
int _loc = (data["stocklocation"]["pk"] ?? -1) as int;
|
||||||
|
|
||||||
|
// A valid stock location!
|
||||||
|
if (_loc > 0) {
|
||||||
|
|
||||||
|
debug("Scanned stock location ${_loc}");
|
||||||
|
|
||||||
|
final bool result = await onLocationScanned(_loc);
|
||||||
|
|
||||||
|
if (result && OneContext.hasContext) {
|
||||||
|
OneContext().pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get to this point, something went wrong during the scan process
|
||||||
|
barcodeFailureTone();
|
||||||
|
|
||||||
|
showSnackIcon(
|
||||||
|
L10().invalidStockLocation,
|
||||||
|
success: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback function which runs when a valid StockLocation is scanned
|
||||||
|
// If this function returns 'true' the barcode scanning dialog will be closed
|
||||||
|
Future<bool> onLocationScanned(int locationId) async {
|
||||||
|
// Re-implement this for particular subclass
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Generic class for scanning a StockItem
|
||||||
|
*
|
||||||
|
* - Validates that the scanned barcode matches a valid StockItem
|
||||||
|
* - Runs a "callback" function if a valid StockItem is found
|
||||||
|
*/
|
||||||
|
class BarcodeScanStockItemHandler extends BarcodeHandler {
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||||
|
// We expect that the barcode points to a 'stockitem'
|
||||||
|
if (data.containsKey("stockitem")) {
|
||||||
|
int _item = (data["stockitem"]["pk"] ?? -1) as int;
|
||||||
|
|
||||||
|
// A valid stock location!
|
||||||
|
if (_item > 0) {
|
||||||
|
|
||||||
|
barcodeSuccessTone();
|
||||||
|
|
||||||
|
final bool result = await onItemScanned(_item);
|
||||||
|
|
||||||
|
if (result && OneContext.hasContext) {
|
||||||
|
OneContext().pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get to this point, something went wrong during the scan process
|
||||||
|
barcodeFailureTone();
|
||||||
|
|
||||||
|
showSnackIcon(
|
||||||
|
L10().invalidStockItem,
|
||||||
|
success: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback function which runs when a valid StockItem is scanned
|
||||||
|
Future<bool> onItemScanned(int itemId) async {
|
||||||
|
// Re-implement this for particular subclass
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Barcode handler for scanning a provided StockItem into a scanned StockLocation.
|
||||||
|
*
|
||||||
|
* - The class is initialized by passing a valid StockItem object
|
||||||
|
* - Expects to scan barcode for a StockLocation
|
||||||
|
* - The StockItem is transferred into the scanned location
|
||||||
|
*/
|
||||||
|
class StockItemScanIntoLocationHandler extends BarcodeScanStockLocationHandler {
|
||||||
|
|
||||||
StockItemScanIntoLocationHandler(this.item);
|
StockItemScanIntoLocationHandler(this.item);
|
||||||
|
|
||||||
final InvenTreeStockItem item;
|
final InvenTreeStockItem item;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
|
Future<bool> onLocationScanned(int locationId) async {
|
||||||
|
|
||||||
@override
|
final result = await item.transferStock(locationId);
|
||||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
|
||||||
// If the barcode points to a "stocklocation", great!
|
|
||||||
if (data.containsKey("stocklocation")) {
|
|
||||||
// Extract location information
|
|
||||||
int location = (data["stocklocation"]["pk"] ?? -1) as int;
|
|
||||||
|
|
||||||
if (location == -1) {
|
|
||||||
showSnackIcon(
|
|
||||||
L10().invalidStockLocation,
|
|
||||||
success: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transfer stock to specified location
|
|
||||||
final result = await item.transferStock(context, location);
|
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|
||||||
barcodeSuccessTone();
|
barcodeSuccessTone();
|
||||||
|
showSnackIcon(L10().barcodeScanIntoLocationSuccess, success: true);
|
||||||
Navigator.of(context).pop();
|
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
L10().barcodeScanIntoLocationSuccess,
|
|
||||||
success: true,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
barcodeFailureTone();
|
barcodeFailureTone();
|
||||||
|
showSnackIcon(L10().barcodeScanIntoLocationFailure, success: false);
|
||||||
showSnackIcon(
|
|
||||||
L10().barcodeScanIntoLocationFailure,
|
|
||||||
success: false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
|
||||||
barcodeFailureTone();
|
return result;
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
L10().invalidStockLocation,
|
|
||||||
success: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class StockLocationScanInItemsHandler extends BarcodeHandler {
|
/*
|
||||||
/*
|
* Barcode handler for scanning stock item(s) into the specified StockLocation.
|
||||||
* Barcode handler for scanning stock item(s) into the specified StockLocation
|
*
|
||||||
|
* - The class is initialized by passing a valid StockLocation object
|
||||||
|
* - Expects to scan a barcode for a StockItem
|
||||||
|
* - The scanned StockItem is transferred into the provided StockLocation
|
||||||
*/
|
*/
|
||||||
|
class StockLocationScanInItemsHandler extends BarcodeScanStockItemHandler {
|
||||||
|
|
||||||
StockLocationScanInItemsHandler(this.location);
|
StockLocationScanInItemsHandler(this.location);
|
||||||
|
|
||||||
@ -341,69 +448,91 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
|
|||||||
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
Future<bool> onItemScanned(int itemId) async {
|
||||||
|
|
||||||
// Returned barcode must match a stock item
|
final InvenTreeStockItem? item = await InvenTreeStockItem().get(itemId) as InvenTreeStockItem?;
|
||||||
if (data.containsKey("stockitem")) {
|
|
||||||
|
|
||||||
int item_id = data["stockitem"]["pk"] as int;
|
bool result = false;
|
||||||
|
|
||||||
final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem?;
|
if (item != null) {
|
||||||
|
|
||||||
if (item == null) {
|
|
||||||
|
|
||||||
|
// Item is already *in* the specified location
|
||||||
|
if (item.locationId == location.pk) {
|
||||||
barcodeFailureTone();
|
barcodeFailureTone();
|
||||||
|
showSnackIcon(L10().itemInLocation, success: true);
|
||||||
showSnackIcon(
|
return false;
|
||||||
L10().invalidStockItem,
|
|
||||||
success: false,
|
|
||||||
);
|
|
||||||
} else if (item.locationId == location.pk) {
|
|
||||||
barcodeFailureTone();
|
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
L10().itemInLocation,
|
|
||||||
success: true
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
final result = await item.transferStock(context, location.pk);
|
result = await item.transferStock(location.pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result) {
|
showSnackIcon(
|
||||||
|
result ? L10().barcodeScanIntoLocationSuccess : L10().barcodeScanIntoLocationFailure,
|
||||||
|
success: result
|
||||||
|
);
|
||||||
|
|
||||||
|
// We always return false here, to ensure the barcode scan dialog remains open
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Barcode handler class for scanning a StockLocation into another StockLocation
|
||||||
|
*
|
||||||
|
* - The class is initialized by passing a valid StockLocation object
|
||||||
|
* - Expects to scan barcode for another *parent* StockLocation
|
||||||
|
* - The scanned StockLocation is set as the "parent" of the provided StockLocation
|
||||||
|
*/
|
||||||
|
class ScanParentLocationHandler extends BarcodeScanStockLocationHandler {
|
||||||
|
|
||||||
|
ScanParentLocationHandler(this.location);
|
||||||
|
|
||||||
|
final InvenTreeStockLocation location;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> onLocationScanned(int locationId) async {
|
||||||
|
|
||||||
|
final response = await location.update(
|
||||||
|
values: {
|
||||||
|
"parent": locationId.toString(),
|
||||||
|
},
|
||||||
|
expectedStatusCode: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 200:
|
||||||
|
case 201:
|
||||||
barcodeSuccessTone();
|
barcodeSuccessTone();
|
||||||
|
showSnackIcon(L10().barcodeScanIntoLocationSuccess, success: true);
|
||||||
showSnackIcon(
|
return true;
|
||||||
L10().barcodeScanIntoLocationSuccess,
|
case 400: // Invalid parent location chosen
|
||||||
success: true
|
barcodeFailureTone();
|
||||||
);
|
showSnackIcon(L10().invalidStockLocation, success: false);
|
||||||
} else {
|
return false;
|
||||||
|
default:
|
||||||
barcodeFailureTone();
|
barcodeFailureTone();
|
||||||
|
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
L10().barcodeScanIntoLocationFailure,
|
L10().barcodeScanIntoLocationFailure,
|
||||||
success: false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
barcodeFailureTone();
|
|
||||||
|
|
||||||
// Does not match a valid stock item!
|
|
||||||
showSnackIcon(
|
|
||||||
L10().invalidStockItem,
|
|
||||||
success: false,
|
success: false,
|
||||||
|
actionText: L10().details,
|
||||||
|
onAction: () {
|
||||||
|
showErrorDialog(
|
||||||
|
L10().barcodeError,
|
||||||
|
response: response,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UniqueBarcodeHandler extends BarcodeHandler {
|
/*
|
||||||
/*
|
|
||||||
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
|
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
|
||||||
*/
|
*/
|
||||||
|
class UniqueBarcodeHandler extends BarcodeHandler {
|
||||||
|
|
||||||
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
|
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
|
||||||
|
|
||||||
@ -422,7 +551,7 @@ class UniqueBarcodeHandler extends BarcodeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||||
|
|
||||||
barcodeFailureTone();
|
barcodeFailureTone();
|
||||||
|
|
||||||
@ -435,7 +564,7 @@ class UniqueBarcodeHandler extends BarcodeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||||
// If the barcode is unknown, we *can* assign it to the stock item!
|
// If the barcode is unknown, we *can* assign it to the stock item!
|
||||||
|
|
||||||
if (!data.containsKey("hash")) {
|
if (!data.containsKey("hash")) {
|
||||||
@ -459,7 +588,9 @@ class UniqueBarcodeHandler extends BarcodeHandler {
|
|||||||
barcodeSuccessTone();
|
barcodeSuccessTone();
|
||||||
|
|
||||||
// Close the barcode scanner
|
// Close the barcode scanner
|
||||||
Navigator.of(context).pop();
|
if (OneContext.hasContext) {
|
||||||
|
OneContext().pop();
|
||||||
|
}
|
||||||
|
|
||||||
callback(hash);
|
callback(hash);
|
||||||
}
|
}
|
||||||
@ -521,7 +652,7 @@ class _QRViewState extends State<InvenTreeQRView> {
|
|||||||
_controller?.pauseCamera();
|
_controller?.pauseCamera();
|
||||||
|
|
||||||
if (barcode.code != null) {
|
if (barcode.code != null) {
|
||||||
_handler.processBarcode(context, _controller, barcode.code ?? "");
|
_handler.processBarcode(_controller, barcode.code ?? "");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,37 @@ import "package:audioplayers/audioplayers.dart";
|
|||||||
import "package:one_context/one_context.dart";
|
import "package:one_context/one_context.dart";
|
||||||
|
|
||||||
|
|
||||||
|
List<String> debug_messages = [];
|
||||||
|
|
||||||
|
void clearDebugMessage() => debug_messages.clear();
|
||||||
|
|
||||||
|
int debugMessageCount() => debug_messages.length;
|
||||||
|
|
||||||
|
// Check if the debug log contains a given message
|
||||||
|
bool debugContains(String msg, {bool raiseAssert = true}) {
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
for (String element in debug_messages) {
|
||||||
|
if (element.contains(msg)) {
|
||||||
|
result = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raiseAssert) {
|
||||||
|
assert(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Display a debug message if we are in testing mode, or running in debug mode
|
* Display a debug message if we are in testing mode, or running in debug mode
|
||||||
*/
|
*/
|
||||||
void debug(dynamic msg) {
|
void debug(dynamic msg) {
|
||||||
|
|
||||||
if (Platform.environment.containsKey("FLUTTER_TEST")) {
|
if (Platform.environment.containsKey("FLUTTER_TEST")) {
|
||||||
|
debug_messages.add(msg.toString());
|
||||||
print("DEBUG: ${msg.toString()}");
|
print("DEBUG: ${msg.toString()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,11 +63,13 @@ String simpleNumberString(double number) {
|
|||||||
*/
|
*/
|
||||||
Future<void> playAudioFile(String path) async {
|
Future<void> playAudioFile(String path) async {
|
||||||
|
|
||||||
|
// Debug message for unit testing
|
||||||
|
debug("Playing audio file: '${path}'");
|
||||||
|
|
||||||
if (!OneContext.hasContext) {
|
if (!OneContext.hasContext) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final player = AudioCache();
|
final player = AudioCache();
|
||||||
player.play(path);
|
player.play(path);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,9 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Launch a modal form to edit the fields available to this model instance.
|
||||||
|
*/
|
||||||
Future<void> editForm(BuildContext context, String title, {Map<String, dynamic> fields=const {}, Function(dynamic)? onSuccess}) async {
|
Future<void> editForm(BuildContext context, String title, {Map<String, dynamic> fields=const {}, Function(dynamic)? onSuccess}) async {
|
||||||
|
|
||||||
if (fields.isEmpty) {
|
if (fields.isEmpty) {
|
||||||
@ -317,7 +320,7 @@ class InvenTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST data to update the model
|
// POST data to update the model
|
||||||
Future<bool> update({Map<String, String> values = const {}}) async {
|
Future<APIResponse> update({Map<String, String> values = const {}, int? expectedStatusCode = 200}) async {
|
||||||
|
|
||||||
var url = path.join(URL, pk.toString());
|
var url = path.join(URL, pk.toString());
|
||||||
|
|
||||||
@ -325,17 +328,13 @@ class InvenTreeModel {
|
|||||||
url += "/";
|
url += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await api.patch(
|
final response = await api.patch(
|
||||||
url,
|
url,
|
||||||
body: values,
|
body: values,
|
||||||
expectedStatusCode: 200
|
expectedStatusCode: expectedStatusCode,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.isValid()) {
|
return response;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the detail view for the associated pk
|
// Return the detail view for the associated pk
|
||||||
|
@ -43,8 +43,8 @@ class InvenTreePartCategory extends InvenTreeModel {
|
|||||||
|
|
||||||
String get pathstring => (jsondata["pathstring"] ?? "") as String;
|
String get pathstring => (jsondata["pathstring"] ?? "") as String;
|
||||||
|
|
||||||
String get parentpathstring {
|
String get parentPathString {
|
||||||
// TODO - Drive the refactor tractor through this
|
|
||||||
List<String> psplit = pathstring.split("/");
|
List<String> psplit = pathstring.split("/");
|
||||||
|
|
||||||
if (psplit.isNotEmpty) {
|
if (psplit.isNotEmpty) {
|
||||||
|
@ -535,7 +535,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
* - Remove
|
* - Remove
|
||||||
* - Count
|
* - Count
|
||||||
*/
|
*/
|
||||||
Future<bool> adjustStock(BuildContext context, String endpoint, double q, {String? notes, int? location}) async {
|
Future<bool> adjustStock(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")
|
||||||
if (isSerialized() && location == null) {
|
if (isSerialized() && location == null) {
|
||||||
@ -566,34 +566,33 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
var response = await api.post(
|
var response = await api.post(
|
||||||
endpoint,
|
endpoint,
|
||||||
body: data,
|
body: data,
|
||||||
expectedStatusCode: 200,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.isValid();
|
return response.isValid() && (response.statusCode == 200 || response.statusCode == 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
|
Future<bool> countStock(double q, {String? notes}) async {
|
||||||
|
|
||||||
final bool result = await adjustStock(context, "/stock/count/", q, notes: notes);
|
final bool result = await adjustStock("/stock/count/", q, notes: notes);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
|
Future<bool> addStock(double q, {String? notes}) async {
|
||||||
|
|
||||||
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
|
final bool result = await adjustStock("/stock/add/", q, notes: notes);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
|
Future<bool> removeStock(double q, {String? notes}) async {
|
||||||
|
|
||||||
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
|
final bool result = await adjustStock("/stock/remove/", q, notes: notes);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> transferStock(BuildContext context, int location, {double? quantity, String? notes}) async {
|
Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
|
||||||
|
|
||||||
double q = this.quantity;
|
double q = this.quantity;
|
||||||
|
|
||||||
@ -602,7 +601,6 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final bool result = await adjustStock(
|
final bool result = await adjustStock(
|
||||||
context,
|
|
||||||
"/stock/transfer/",
|
"/stock/transfer/",
|
||||||
q,
|
q,
|
||||||
notes: notes,
|
notes: notes,
|
||||||
@ -653,12 +651,14 @@ class InvenTreeStockLocation extends InvenTreeModel {
|
|||||||
return {
|
return {
|
||||||
"name": {},
|
"name": {},
|
||||||
"description": {},
|
"description": {},
|
||||||
"parent": {},
|
"parent": {
|
||||||
|
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
String get parentpathstring {
|
String get parentPathString {
|
||||||
// TODO - Drive the refactor tractor through this
|
|
||||||
List<String> psplit = pathstring.split("/");
|
List<String> psplit = pathstring.split("/");
|
||||||
|
|
||||||
if (psplit.isNotEmpty) {
|
if (psplit.isNotEmpty) {
|
||||||
|
@ -82,6 +82,9 @@
|
|||||||
"barcodeAssign": "Assign Barcode",
|
"barcodeAssign": "Assign Barcode",
|
||||||
"@barcodeAssign": {},
|
"@barcodeAssign": {},
|
||||||
|
|
||||||
|
"barcodeAssignDetail": "Scan custom barcode to assign",
|
||||||
|
"@barcodeAssignDetail": {},
|
||||||
|
|
||||||
"barcodeAssigned": "Barcode assigned",
|
"barcodeAssigned": "Barcode assigned",
|
||||||
"@barcodeAssigned": {},
|
"@barcodeAssigned": {},
|
||||||
|
|
||||||
@ -106,7 +109,7 @@
|
|||||||
"barcodeScanGeneral": "Scan an InvenTree barcode",
|
"barcodeScanGeneral": "Scan an InvenTree barcode",
|
||||||
"@barcodeScanGeneral": {},
|
"@barcodeScanGeneral": {},
|
||||||
|
|
||||||
"barcodeScanInItems": "Scan stock items into location",
|
"barcodeScanInItems": "Scan stock items into this location",
|
||||||
"@barcodeScanInItems": {},
|
"@barcodeScanInItems": {},
|
||||||
|
|
||||||
"barcodeScanLocation": "Scan stock location",
|
"barcodeScanLocation": "Scan stock location",
|
||||||
@ -807,6 +810,9 @@
|
|||||||
"scanIntoLocation": "Scan Into Location",
|
"scanIntoLocation": "Scan Into Location",
|
||||||
"@scanIntoLocation": {},
|
"@scanIntoLocation": {},
|
||||||
|
|
||||||
|
"scanIntoLocationDetail": "Scan this item into location",
|
||||||
|
"@scanIntoLocationDetail": {},
|
||||||
|
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"@search": {
|
"@search": {
|
||||||
"description": "search"
|
"description": "search"
|
||||||
@ -1087,6 +1093,15 @@
|
|||||||
"description": "transfer stock"
|
"description": "transfer stock"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"transferStockDetail": "Transfer item to a different location",
|
||||||
|
"@transferStockDetail": {},
|
||||||
|
|
||||||
|
"transferStockLocation": "Transfer Stock Location",
|
||||||
|
"@transferStockLocation": {},
|
||||||
|
|
||||||
|
"transferStockLocationDetail": "Transfer this stock location into another",
|
||||||
|
"@transferStockLocationDetail": {},
|
||||||
|
|
||||||
"translate": "Translate",
|
"translate": "Translate",
|
||||||
"@translate": {},
|
"@translate": {},
|
||||||
|
|
||||||
|
@ -57,6 +57,9 @@ class UserProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Class for storing and managing user (server) profiles
|
||||||
|
*/
|
||||||
class UserProfileDBManager {
|
class UserProfileDBManager {
|
||||||
|
|
||||||
final store = StoreRef("profiles");
|
final store = StoreRef("profiles");
|
||||||
@ -96,6 +99,8 @@ class UserProfileDBManager {
|
|||||||
if (exists) {
|
if (exists) {
|
||||||
debug("addProfile() : UserProfile '${profile.name}' already exists");
|
debug("addProfile() : UserProfile '${profile.name}' already exists");
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
debug("Adding new profile: '${profile.name}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
int key = await store.add(await _db, profile.toJson()) as int;
|
int key = await store.add(await _db, profile.toJson()) as int;
|
||||||
@ -149,8 +154,6 @@ class UserProfileDBManager {
|
|||||||
|
|
||||||
for (int idx = 0; idx < profiles.length; idx++) {
|
for (int idx = 0; idx < profiles.length; idx++) {
|
||||||
|
|
||||||
debug("- Checking ${idx} - key = ${profiles[idx].key} - ${profiles[idx].value.toString()}");
|
|
||||||
|
|
||||||
if (profiles[idx].key is int && profiles[idx].key == selected) {
|
if (profiles[idx].key is int && profiles[idx].key == selected) {
|
||||||
return UserProfile.fromJson(
|
return UserProfile.fromJson(
|
||||||
profiles[idx].key as int,
|
profiles[idx].key as int,
|
||||||
@ -190,6 +193,24 @@ class UserProfileDBManager {
|
|||||||
return profileList;
|
return profileList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Retrieve a profile by name (or null if no match exists)
|
||||||
|
*/
|
||||||
|
Future<UserProfile?> getProfileByName(String name) async {
|
||||||
|
final profiles = await getAllProfiles();
|
||||||
|
|
||||||
|
UserProfile? prf;
|
||||||
|
|
||||||
|
for (UserProfile profile in profiles) {
|
||||||
|
if (profile.name == name) {
|
||||||
|
prf = profile;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prf;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Mark the particular profile as selected
|
* Mark the particular profile as selected
|
||||||
*/
|
*/
|
||||||
|
@ -131,7 +131,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
children.add(
|
children.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().parentCategory),
|
title: Text(L10().parentCategory),
|
||||||
subtitle: Text("${category?.parentpathstring}"),
|
subtitle: Text("${category?.parentPathString}"),
|
||||||
leading: FaIcon(
|
leading: FaIcon(
|
||||||
FontAwesomeIcons.levelUpAlt,
|
FontAwesomeIcons.levelUpAlt,
|
||||||
color: COLOR_CLICK,
|
color: COLOR_CLICK,
|
||||||
|
@ -3,11 +3,15 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
|||||||
import "package:inventree/helpers.dart";
|
import "package:inventree/helpers.dart";
|
||||||
import "package:one_context/one_context.dart";
|
import "package:one_context/one_context.dart";
|
||||||
|
|
||||||
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
import "package:inventree/preferences.dart";
|
import "package:inventree/preferences.dart";
|
||||||
import "package:inventree/widget/snacks.dart";
|
import "package:inventree/widget/snacks.dart";
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Display a "confirmation" dialog allowing the user to accept or reject an action
|
||||||
|
*/
|
||||||
Future<void> confirmationDialog(String title, String text, {IconData icon = FontAwesomeIcons.questionCircle, String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async {
|
Future<void> confirmationDialog(String title, String text, {IconData icon = FontAwesomeIcons.questionCircle, String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async {
|
||||||
|
|
||||||
String _accept = acceptText ?? L10().ok;
|
String _accept = acceptText ?? L10().ok;
|
||||||
@ -51,22 +55,86 @@ Future<void> confirmationDialog(String title, String text, {IconData icon = Font
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> showErrorDialog(String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String? error, Function? onDismissed}) async {
|
/*
|
||||||
|
* Construct an error dialog showing information to the user
|
||||||
|
*
|
||||||
|
* @title = Title to be displayed at the top of the dialog
|
||||||
|
* @description = Simple string description of error
|
||||||
|
* @data = Error response (e.g from server)
|
||||||
|
*/
|
||||||
|
Future<void> showErrorDialog(String title, {String description = "", APIResponse? response, IconData icon = FontAwesomeIcons.exclamationCircle, Function? onDismissed}) async {
|
||||||
|
|
||||||
String _error = error ?? L10().error;
|
List<Widget> children = [];
|
||||||
|
|
||||||
|
if (description.isNotEmpty) {
|
||||||
|
children.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(description),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (response != null) {
|
||||||
|
// Look for extra error information in the provided APIResponse object
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 400: // Bad request (typically bad input)
|
||||||
|
if (response.data is Map<String, dynamic>) {
|
||||||
|
for (String field in response.data.keys) {
|
||||||
|
|
||||||
|
dynamic error = response.data[field];
|
||||||
|
|
||||||
|
if (error is List) {
|
||||||
|
for (int ii = 0; ii < error.length; ii++) {
|
||||||
|
children.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(field),
|
||||||
|
subtitle: Text(error[ii].toString()),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
children.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(field),
|
||||||
|
subtitle: Text(response.data[field].toString()),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
children.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().responseInvalid),
|
||||||
|
subtitle: Text(response.data.toString())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unhandled server response
|
||||||
|
children.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().statusCode),
|
||||||
|
subtitle: Text(response.statusCode.toString()),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
children.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().responseData),
|
||||||
|
subtitle: Text(response.data.toString()),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OneContext().showDialog(
|
OneContext().showDialog(
|
||||||
builder: (context) => SimpleDialog(
|
builder: (context) => SimpleDialog(
|
||||||
title: ListTile(
|
title: ListTile(
|
||||||
title: Text(_error),
|
title: Text(title),
|
||||||
leading: FaIcon(icon),
|
leading: FaIcon(icon),
|
||||||
),
|
),
|
||||||
children: [
|
children: children
|
||||||
ListTile(
|
|
||||||
title: Text(title),
|
|
||||||
subtitle: Text(description),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (onDismissed != null) {
|
if (onDismissed != null) {
|
||||||
@ -106,9 +174,8 @@ Future<void> showServerError(String url, String title, String description) async
|
|||||||
actionText: L10().details,
|
actionText: L10().details,
|
||||||
onAction: () {
|
onAction: () {
|
||||||
showErrorDialog(
|
showErrorDialog(
|
||||||
title,
|
L10().serverError,
|
||||||
description,
|
description: description,
|
||||||
error: L10().serverError,
|
|
||||||
icon: FontAwesomeIcons.server
|
icon: FontAwesomeIcons.server
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -40,27 +40,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
|
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
|
|
||||||
/*
|
|
||||||
actions.add(
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(FontAwesomeIcons.search),
|
|
||||||
onPressed: () {
|
|
||||||
|
|
||||||
Map<String, String> filters = {};
|
|
||||||
|
|
||||||
if (location != null) {
|
|
||||||
filters["location"] = "${location.pk}";
|
|
||||||
}
|
|
||||||
|
|
||||||
showSearch(
|
|
||||||
context: context,
|
|
||||||
delegate: StockSearchDelegate(context, filters: filters)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
|
|
||||||
// Add "locate" button
|
// Add "locate" button
|
||||||
@ -252,7 +231,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
children.add(
|
children.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().parentLocation),
|
title: Text(L10().parentLocation),
|
||||||
subtitle: Text("${location!.parentpathstring}"),
|
subtitle: Text("${location!.parentPathString}"),
|
||||||
leading: FaIcon(FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK),
|
leading: FaIcon(FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
||||||
@ -381,6 +360,7 @@ List<Widget> detailTiles() {
|
|||||||
title: Text(L10().locationCreate),
|
title: Text(L10().locationCreate),
|
||||||
subtitle: Text(L10().locationCreateDetail),
|
subtitle: Text(L10().locationCreateDetail),
|
||||||
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
|
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
|
||||||
|
trailing: FaIcon(FontAwesomeIcons.plusCircle, color: COLOR_CLICK),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
_newLocation(context);
|
_newLocation(context);
|
||||||
},
|
},
|
||||||
@ -392,6 +372,7 @@ List<Widget> detailTiles() {
|
|||||||
title: Text(L10().stockItemCreate),
|
title: Text(L10().stockItemCreate),
|
||||||
subtitle: Text(L10().stockItemCreateDetail),
|
subtitle: Text(L10().stockItemCreateDetail),
|
||||||
leading: FaIcon(FontAwesomeIcons.boxes, color: COLOR_CLICK),
|
leading: FaIcon(FontAwesomeIcons.boxes, color: COLOR_CLICK),
|
||||||
|
trailing: FaIcon(FontAwesomeIcons.plusCircle, color: COLOR_CLICK),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
_newStockItem(context);
|
_newStockItem(context);
|
||||||
},
|
},
|
||||||
@ -401,14 +382,15 @@ List<Widget> detailTiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
// Stock adjustment actions
|
|
||||||
|
// Scan stock item into location
|
||||||
if (InvenTreeAPI().checkPermission("stock", "change")) {
|
if (InvenTreeAPI().checkPermission("stock", "change")) {
|
||||||
// Scan items into location
|
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().barcodeScanInItems),
|
title: Text(L10().barcodeScanItem),
|
||||||
|
subtitle: Text(L10().barcodeScanInItems),
|
||||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||||
trailing: Icon(Icons.qr_code),
|
trailing: Icon(Icons.qr_code, color: COLOR_CLICK),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
||||||
var _loc = location;
|
var _loc = location;
|
||||||
@ -426,20 +408,34 @@ List<Widget> detailTiles() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move location into another location
|
// Scan this location into another one
|
||||||
// TODO: Implement this!
|
if (InvenTreeAPI().checkPermission("stock_location", "change")) {
|
||||||
/*
|
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Move Stock Location"),
|
title: Text(L10().transferStockLocation),
|
||||||
leading: FaIcon(FontAwesomeIcons.sitemap),
|
subtitle: Text(L10().transferStockLocationDetail),
|
||||||
trailing: Icon(Icons.qr_code),
|
leading: FaIcon(FontAwesomeIcons.signInAlt, color: COLOR_CLICK),
|
||||||
|
trailing: Icon(Icons.qr_code, color: COLOR_CLICK),
|
||||||
|
onTap: () {
|
||||||
|
var _loc = location;
|
||||||
|
|
||||||
|
if (_loc != null) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) =>
|
||||||
|
InvenTreeQRView(
|
||||||
|
ScanParentLocationHandler(_loc)))
|
||||||
|
).then((value) {
|
||||||
|
refresh(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
*/
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (tiles.length <= 1) {
|
if (tiles.length <= 1) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
|
@ -381,7 +381,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("${part.keywords}"),
|
title: Text("${part.keywords}"),
|
||||||
leading: FaIcon(FontAwesomeIcons.key),
|
leading: FaIcon(FontAwesomeIcons.tags),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -210,6 +210,9 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Receive a specified PurchaseOrderLineItem into stock
|
||||||
|
*/
|
||||||
void receiveLine(BuildContext context, InvenTreePOLineItem lineItem) {
|
void receiveLine(BuildContext context, InvenTreePOLineItem lineItem) {
|
||||||
|
|
||||||
Map<String, dynamic> fields = {
|
Map<String, dynamic> fields = {
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import "package:one_context/one_context.dart";
|
import "package:one_context/one_context.dart";
|
||||||
import "package:inventree/l10.dart";
|
|
||||||
|
|
||||||
|
import "package:inventree/helpers.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Display a configurable 'snackbar' at the bottom of the screen
|
* Display a configurable 'snackbar' at the bottom of the screen
|
||||||
*/
|
*/
|
||||||
void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) {
|
void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) {
|
||||||
|
|
||||||
|
debug("showSnackIcon: '${text}'");
|
||||||
|
|
||||||
// Escape quickly if we do not have context
|
// Escape quickly if we do not have context
|
||||||
if (!OneContext.hasContext) {
|
if (!OneContext.hasContext) {
|
||||||
|
// Debug message for unit testing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,20 +402,23 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
|
|
||||||
Future<void> _unassignBarcode(BuildContext context) async {
|
Future<void> _unassignBarcode(BuildContext context) async {
|
||||||
|
|
||||||
final bool result = await item.update(values: {"uid": ""});
|
final response = await item.update(values: {"uid": ""});
|
||||||
|
|
||||||
if (result) {
|
switch (response.statusCode) {
|
||||||
|
case 200:
|
||||||
|
case 201:
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
L10().stockItemUpdateSuccess,
|
L10().stockItemUpdateSuccess,
|
||||||
success: true
|
success: true
|
||||||
);
|
);
|
||||||
} else {
|
break;
|
||||||
|
default:
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
L10().stockItemUpdateFailure,
|
L10().stockItemUpdateFailure,
|
||||||
success: false,
|
success: false,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh(context);
|
refresh(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -779,6 +782,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().transferStock),
|
title: Text(L10().transferStock),
|
||||||
|
subtitle: Text(L10().transferStockDetail),
|
||||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||||
onTap: () { _transferStockDialog(context); },
|
onTap: () { _transferStockDialog(context); },
|
||||||
)
|
)
|
||||||
@ -788,6 +792,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().scanIntoLocation),
|
title: Text(L10().scanIntoLocation),
|
||||||
|
subtitle: Text(L10().scanIntoLocationDetail),
|
||||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||||
trailing: Icon(Icons.qr_code_scanner),
|
trailing: Icon(Icons.qr_code_scanner),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -806,6 +811,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().barcodeAssign),
|
title: Text(L10().barcodeAssign),
|
||||||
|
subtitle: Text(L10().barcodeAssignDetail),
|
||||||
leading: Icon(Icons.qr_code),
|
leading: Icon(Icons.qr_code),
|
||||||
trailing: Icon(Icons.qr_code_scanner),
|
trailing: Icon(Icons.qr_code_scanner),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -815,8 +821,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
values: {
|
values: {
|
||||||
"uid": hash,
|
"uid": hash,
|
||||||
}
|
}
|
||||||
).then((result) {
|
).then((response) {
|
||||||
if (result) {
|
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 200:
|
||||||
|
case 201:
|
||||||
barcodeSuccessTone();
|
barcodeSuccessTone();
|
||||||
|
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
@ -826,6 +835,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
refresh(context);
|
refresh(context);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -59,7 +59,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
|||||||
Map<String, String> get orderingOptions => {
|
Map<String, String> get orderingOptions => {
|
||||||
"part__name": L10().name,
|
"part__name": L10().name,
|
||||||
"part__IPN": L10().internalPartNumber,
|
"part__IPN": L10().internalPartNumber,
|
||||||
"quantity": L10().quantity,
|
"stock": L10().quantity,
|
||||||
"status": L10().status,
|
"status": L10().status,
|
||||||
"batch": L10().batchCode,
|
"batch": L10().batchCode,
|
||||||
"updated": L10().lastUpdated,
|
"updated": L10().lastUpdated,
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import "package:test/test.dart";
|
import "package:test/test.dart";
|
||||||
|
|
||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
|
import "package:inventree/helpers.dart";
|
||||||
import "package:inventree/user_profile.dart";
|
import "package:inventree/user_profile.dart";
|
||||||
|
|
||||||
|
|
||||||
@ -94,6 +95,9 @@ void main() {
|
|||||||
|
|
||||||
assert(!api.checkConnection());
|
assert(!api.checkConnection());
|
||||||
|
|
||||||
|
debugContains("Token request failed: STATUS 401");
|
||||||
|
debugContains("showSnackIcon: 'Not Connected'");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
assert(false);
|
assert(false);
|
||||||
}
|
}
|
||||||
@ -137,6 +141,9 @@ void main() {
|
|||||||
assert(api.checkPermission("stocklocation", "delete"));
|
assert(api.checkPermission("stocklocation", "delete"));
|
||||||
assert(!api.checkPermission("part", "weirdpermission"));
|
assert(!api.checkPermission("part", "weirdpermission"));
|
||||||
assert(api.checkPermission("blah", "bloo"));
|
assert(api.checkPermission("blah", "bloo"));
|
||||||
|
|
||||||
|
debugContains("Received token from server");
|
||||||
|
debugContains("showSnackIcon: 'Connected to Server'");
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
160
test/barcode_test.dart
Normal file
160
test/barcode_test.dart
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* Unit testing for barcode scanning functionality.
|
||||||
|
*
|
||||||
|
* As the unit testing framework cannot really "scan" barcode data,
|
||||||
|
* we will mock the scanned data by passing raw "barcode" data to the scanning framework.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "package:flutter_test/flutter_test.dart";
|
||||||
|
|
||||||
|
import "package:inventree/api.dart";
|
||||||
|
import "package:inventree/barcode.dart";
|
||||||
|
import "package:inventree/helpers.dart";
|
||||||
|
import "package:inventree/user_profile.dart";
|
||||||
|
|
||||||
|
import "package:inventree/inventree/stock.dart";
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
|
||||||
|
// Connect to the server
|
||||||
|
setUpAll(() async {
|
||||||
|
final prf = await UserProfileDBManager().getProfileByName("Test Profile");
|
||||||
|
|
||||||
|
if (prf != null) {
|
||||||
|
UserProfileDBManager().deleteProfile(prf);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = await UserProfileDBManager().addProfile(
|
||||||
|
UserProfile(
|
||||||
|
name: "Test Profile",
|
||||||
|
server: "http://localhost:12345",
|
||||||
|
username: "testuser",
|
||||||
|
password: "testpassword",
|
||||||
|
selected: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(result);
|
||||||
|
|
||||||
|
assert(await UserProfileDBManager().selectProfileByName("Test Profile"));
|
||||||
|
assert(await InvenTreeAPI().connectToServer());
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
// Clear the debug log
|
||||||
|
clearDebugMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Test BarcodeScanHandler:", () {
|
||||||
|
// Tests for scanning a "generic" barcode
|
||||||
|
|
||||||
|
var handler = BarcodeScanHandler();
|
||||||
|
|
||||||
|
test("Empty Barcode", () async {
|
||||||
|
// Handle an 'empty' barcode
|
||||||
|
await handler.processBarcode(null, "");
|
||||||
|
|
||||||
|
debugContains("Scanned barcode data: ''");
|
||||||
|
debugContains("showSnackIcon: 'Barcode scan error'");
|
||||||
|
|
||||||
|
assert(debugMessageCount() == 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Junk Data", () async {
|
||||||
|
// test scanning 'junk' data
|
||||||
|
|
||||||
|
await handler.processBarcode(null, "abcdefg");
|
||||||
|
|
||||||
|
debugContains("Scanned barcode data: 'abcdefg'");
|
||||||
|
debugContains("showSnackIcon: 'No match for barcode'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Invalid StockLocation", () async {
|
||||||
|
// Scan an invalid stock location
|
||||||
|
await handler.processBarcode(null, '{"stocklocation": 999999}');
|
||||||
|
|
||||||
|
debugContains("Scanned barcode data: '{\"stocklocation\": 999999}'");
|
||||||
|
debugContains("showSnackIcon: 'No match for barcode'");
|
||||||
|
assert(debugMessageCount() == 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Test StockItemScanIntoLocationHandler:", () {
|
||||||
|
// Tests for scanning a stock item into a location
|
||||||
|
|
||||||
|
test("Scan Into Location", () async {
|
||||||
|
|
||||||
|
final item = await InvenTreeStockItem().get(1) as InvenTreeStockItem?;
|
||||||
|
|
||||||
|
assert(item != null);
|
||||||
|
assert(item!.pk == 1);
|
||||||
|
|
||||||
|
var handler = StockItemScanIntoLocationHandler(item!);
|
||||||
|
|
||||||
|
await handler.processBarcode(null, '{"stocklocation": 7}');
|
||||||
|
// Check the location has been updated
|
||||||
|
await item.reload();
|
||||||
|
assert(item.locationId == 7);
|
||||||
|
|
||||||
|
debugContains("Scanned stock location 7");
|
||||||
|
|
||||||
|
// Scan into a new location
|
||||||
|
await handler.processBarcode(null, '{"stocklocation": 1}');
|
||||||
|
await item.reload();
|
||||||
|
assert(item.locationId == 1);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Test StockLocationScanInItemsHandler:", () {
|
||||||
|
// Tests for scanning items into a stock location
|
||||||
|
|
||||||
|
test("Scan In Items", () async {
|
||||||
|
final location = await InvenTreeStockLocation().get(1) as InvenTreeStockLocation?;
|
||||||
|
|
||||||
|
assert(location != null);
|
||||||
|
assert(location!.pk == 1);
|
||||||
|
|
||||||
|
var handler = StockLocationScanInItemsHandler(location!);
|
||||||
|
|
||||||
|
// Scan multiple items into this location
|
||||||
|
for (int id in [1, 2, 11]) {
|
||||||
|
await handler.processBarcode(null, '{"stockitem": ${id}}');
|
||||||
|
|
||||||
|
var item = await InvenTreeStockItem().get(id) as InvenTreeStockItem?;
|
||||||
|
|
||||||
|
assert(item != null);
|
||||||
|
assert(item!.pk == id);
|
||||||
|
assert(item!.locationId == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Test ScanParentLocationHandler:", () {
|
||||||
|
// Tests for scanning a location into a parent location
|
||||||
|
|
||||||
|
test("Scan Parent", () async {
|
||||||
|
final location = await InvenTreeStockLocation().get(7) as InvenTreeStockLocation?;
|
||||||
|
|
||||||
|
assert(location != null);
|
||||||
|
assert(location!.pk == 7);
|
||||||
|
assert(location!.parentId == 4);
|
||||||
|
|
||||||
|
var handler = ScanParentLocationHandler(location!);
|
||||||
|
|
||||||
|
// Scan into new parent location
|
||||||
|
await handler.processBarcode(null, '{"stocklocation": 1}');
|
||||||
|
await location.reload();
|
||||||
|
assert(location.parentId == 1);
|
||||||
|
|
||||||
|
// Scan back into old parent location
|
||||||
|
await handler.processBarcode(null, '{"stocklocation": 4}');
|
||||||
|
await location.reload();
|
||||||
|
assert(location.parentId == 4);
|
||||||
|
|
||||||
|
debugContains("showSnackIcon: 'Scanned into location'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -2,11 +2,11 @@
|
|||||||
* Unit tests for accessing various model classes via the API
|
* Unit tests for accessing various model classes via the API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "package:inventree/inventree/model.dart";
|
|
||||||
import "package:test/test.dart";
|
import "package:test/test.dart";
|
||||||
|
|
||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/user_profile.dart";
|
import "package:inventree/user_profile.dart";
|
||||||
|
import "package:inventree/inventree/model.dart";
|
||||||
import "package:inventree/inventree/part.dart";
|
import "package:inventree/inventree/part.dart";
|
||||||
|
|
||||||
|
|
||||||
@ -112,32 +112,40 @@ void main() {
|
|||||||
assert(result != null);
|
assert(result != null);
|
||||||
assert(result is InvenTreePart);
|
assert(result is InvenTreePart);
|
||||||
|
|
||||||
|
APIResponse? response;
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
InvenTreePart part = result as InvenTreePart;
|
InvenTreePart part = result as InvenTreePart;
|
||||||
assert(part.name == "M2x4 LPHS");
|
assert(part.name == "M2x4 LPHS");
|
||||||
|
|
||||||
// Change the name to something else
|
// Change the name to something else
|
||||||
assert(await part.update(
|
|
||||||
|
response = await part.update(
|
||||||
values: {
|
values: {
|
||||||
"name": "Woogle",
|
"name": "Woogle",
|
||||||
}
|
}
|
||||||
));
|
);
|
||||||
|
|
||||||
|
assert(response.isValid());
|
||||||
|
assert(response.statusCode == 200);
|
||||||
|
|
||||||
assert(await part.reload());
|
assert(await part.reload());
|
||||||
assert(part.name == "Woogle");
|
assert(part.name == "Woogle");
|
||||||
|
|
||||||
// And change it back again
|
// And change it back again
|
||||||
assert(await part.update(
|
response = await part.update(
|
||||||
values: {
|
values: {
|
||||||
"name": "M2x4 LPHS"
|
"name": "M2x4 LPHS"
|
||||||
}
|
}
|
||||||
));
|
);
|
||||||
|
|
||||||
|
assert(response.isValid());
|
||||||
|
assert(response.statusCode == 200);
|
||||||
|
|
||||||
assert(await part.reload());
|
assert(await part.reload());
|
||||||
assert(part.name == "M2x4 LPHS");
|
assert(part.name == "M2x4 LPHS");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user