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
|
||||
- Indicate available quantity in stock detail view
|
||||
- Adds configurable filtering to various list views
|
||||
- Allow stock location to be "scanned" into another location using barcode
|
||||
|
||||
### 0.7.3 - June 2022
|
||||
---
|
||||
|
@ -413,6 +413,8 @@ class InvenTreeAPI {
|
||||
break;
|
||||
}
|
||||
|
||||
debug("Token request failed: STATUS ${response.statusCode}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1017,14 +1019,16 @@ class InvenTreeAPI {
|
||||
}
|
||||
|
||||
if (statusCode != null) {
|
||||
|
||||
// Expected status code not returned
|
||||
if (statusCode != _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) {
|
||||
showServerError(url, L10().connectionRefused, error.toString());
|
||||
response.error = "SocketException";
|
||||
|
@ -857,6 +857,7 @@ Future<void> launchApiForm(
|
||||
Map<String, dynamic> serverFields = {};
|
||||
|
||||
if (url.isNotEmpty) {
|
||||
|
||||
var options = await InvenTreeAPI().options(url);
|
||||
|
||||
// Invalid response from server
|
||||
|
419
lib/barcode.dart
419
lib/barcode.dart
@ -43,14 +43,13 @@ Future <void> barcodeFailureTone() async {
|
||||
}
|
||||
|
||||
|
||||
/* Generic class which "handles" a barcode, by communicating with the InvenTree server,
|
||||
* and handling match / unknown / error cases.
|
||||
*
|
||||
* Override functionality of this class to perform custom actions,
|
||||
* based on the response returned from the InvenTree server
|
||||
*/
|
||||
class BarcodeHandler {
|
||||
/*
|
||||
* Class which "handles" a barcode, by communicating with the InvenTree server,
|
||||
* and handling match / unknown / error cases.
|
||||
*
|
||||
* Override functionality of this class to perform custom actions,
|
||||
* based on the response returned from the InvenTree server
|
||||
*/
|
||||
|
||||
BarcodeHandler();
|
||||
|
||||
@ -58,12 +57,12 @@ class BarcodeHandler {
|
||||
|
||||
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
|
||||
// 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
|
||||
// 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();
|
||||
|
||||
@ -86,12 +85,27 @@ class BarcodeHandler {
|
||||
_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;
|
||||
|
||||
print("Scanned barcode data: ${barcode}");
|
||||
debug("Scanned barcode data: '${barcode}'");
|
||||
|
||||
barcode = barcode.trim();
|
||||
|
||||
// Empty barcode is invalid
|
||||
if (barcode.isEmpty) {
|
||||
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeError,
|
||||
icon: FontAwesomeIcons.exclamationCircle,
|
||||
success: false
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -109,7 +123,9 @@ class BarcodeHandler {
|
||||
|
||||
// Handle strange response from the server
|
||||
if (!response.isValid() || !response.isMap()) {
|
||||
onBarcodeUnknown(context, {});
|
||||
onBarcodeUnknown({});
|
||||
|
||||
showSnackIcon(L10().serverError, success: false);
|
||||
|
||||
// We want to know about this one!
|
||||
await sentryReportMessage(
|
||||
@ -122,31 +138,36 @@ class BarcodeHandler {
|
||||
"valid": response.isValid().toString(),
|
||||
"error": response.error,
|
||||
"errorDetail": response.errorDetail,
|
||||
"overlayText": getOverlayText(context),
|
||||
"className": "${this}",
|
||||
}
|
||||
);
|
||||
} else if ((response.statusCode >= 400) || data.containsKey("error")) {
|
||||
onBarcodeUnknown(context, data);
|
||||
} else if (data.containsKey("success")) {
|
||||
onBarcodeMatched(context, data);
|
||||
await onBarcodeMatched(data);
|
||||
} else if ((response.statusCode >= 400) || data.containsKey("error")) {
|
||||
await onBarcodeUnknown(data);
|
||||
} else {
|
||||
onBarcodeUnhandled(context, data);
|
||||
await onBarcodeUnhandled(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Class for general barcode scanning.
|
||||
* Scan *any* barcode without context, and then redirect app to correct view.
|
||||
*
|
||||
* Handles scanning of:
|
||||
*
|
||||
* - StockLocation
|
||||
* - StockItem
|
||||
* - Part
|
||||
*/
|
||||
class BarcodeScanHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Class for general barcode scanning.
|
||||
* Scan *any* barcode without context, and then redirect app to correct view
|
||||
*/
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanGeneral;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||
|
||||
barcodeFailureTone();
|
||||
|
||||
@ -158,7 +179,7 @@ class BarcodeScanHandler extends BarcodeHandler {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
|
||||
int pk = -1;
|
||||
|
||||
@ -173,8 +194,13 @@ class BarcodeScanHandler extends BarcodeHandler {
|
||||
|
||||
InvenTreeStockLocation().get(pk).then((var loc) {
|
||||
if (loc is InvenTreeStockLocation) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
|
||||
showSnackIcon(
|
||||
L10().stockLocation,
|
||||
success: true,
|
||||
icon: Icons.qr_code,
|
||||
);
|
||||
OneContext().pop();
|
||||
OneContext().navigator.push(MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -196,13 +222,15 @@ class BarcodeScanHandler extends BarcodeHandler {
|
||||
barcodeSuccessTone();
|
||||
|
||||
InvenTreeStockItem().get(pk).then((var item) {
|
||||
|
||||
// Dispose of the barcode scanner
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (item is InvenTreeStockItem) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
|
||||
}
|
||||
showSnackIcon(
|
||||
L10().stockItem,
|
||||
success: true,
|
||||
icon: Icons.qr_code,
|
||||
);
|
||||
OneContext().pop();
|
||||
if (item is InvenTreeStockItem) {
|
||||
OneContext().push(MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -222,13 +250,17 @@ class BarcodeScanHandler extends BarcodeHandler {
|
||||
barcodeSuccessTone();
|
||||
|
||||
InvenTreePart().get(pk).then((var part) {
|
||||
showSnackIcon(
|
||||
L10().part,
|
||||
success: true,
|
||||
icon: Icons.qr_code,
|
||||
);
|
||||
// Dismiss the barcode scanner
|
||||
OneContext().pop();
|
||||
|
||||
// Dismiss the barcode scanner
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (part is InvenTreePart) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
||||
}
|
||||
if (part is InvenTreePart) {
|
||||
OneContext().push(MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
||||
}
|
||||
});
|
||||
} 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);
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
|
||||
Future<bool> onLocationScanned(int locationId) async {
|
||||
|
||||
@override
|
||||
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;
|
||||
final result = await item.transferStock(locationId);
|
||||
|
||||
if (location == -1) {
|
||||
showSnackIcon(
|
||||
L10().invalidStockLocation,
|
||||
success: false,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Transfer stock to specified location
|
||||
final result = await item.transferStock(context, location);
|
||||
|
||||
if (result) {
|
||||
|
||||
barcodeSuccessTone();
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationSuccess,
|
||||
success: true,
|
||||
);
|
||||
} else {
|
||||
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationFailure,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
if (result) {
|
||||
barcodeSuccessTone();
|
||||
showSnackIcon(L10().barcodeScanIntoLocationSuccess, success: true);
|
||||
} else {
|
||||
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().invalidStockLocation,
|
||||
success: false,
|
||||
);
|
||||
showSnackIcon(L10().barcodeScanIntoLocationFailure, success: false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
||||
@ -341,69 +448,91 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
Future<bool> onItemScanned(int itemId) async {
|
||||
|
||||
// Returned barcode must match a stock item
|
||||
if (data.containsKey("stockitem")) {
|
||||
final InvenTreeStockItem? item = await InvenTreeStockItem().get(itemId) as InvenTreeStockItem?;
|
||||
|
||||
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();
|
||||
|
||||
showSnackIcon(
|
||||
L10().invalidStockItem,
|
||||
success: false,
|
||||
);
|
||||
} else if (item.locationId == location.pk) {
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().itemInLocation,
|
||||
success: true
|
||||
);
|
||||
showSnackIcon(L10().itemInLocation, success: true);
|
||||
return false;
|
||||
} else {
|
||||
final result = await item.transferStock(context, location.pk);
|
||||
|
||||
if (result) {
|
||||
|
||||
barcodeSuccessTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationSuccess,
|
||||
success: true
|
||||
);
|
||||
} else {
|
||||
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationFailure,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
result = await item.transferStock(location.pk);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(
|
||||
result ? L10().barcodeScanIntoLocationSuccess : L10().barcodeScanIntoLocationFailure,
|
||||
success: result
|
||||
);
|
||||
|
||||
// Does not match a valid stock item!
|
||||
showSnackIcon(
|
||||
L10().invalidStockItem,
|
||||
success: false,
|
||||
);
|
||||
// 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();
|
||||
showSnackIcon(L10().barcodeScanIntoLocationSuccess, success: true);
|
||||
return true;
|
||||
case 400: // Invalid parent location chosen
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(L10().invalidStockLocation, success: false);
|
||||
return false;
|
||||
default:
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationFailure,
|
||||
success: false,
|
||||
actionText: L10().details,
|
||||
onAction: () {
|
||||
showErrorDialog(
|
||||
L10().barcodeError,
|
||||
response: response,
|
||||
);
|
||||
}
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
|
||||
*/
|
||||
class UniqueBarcodeHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
|
||||
*/
|
||||
|
||||
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
|
||||
|
||||
@ -422,7 +551,7 @@ class UniqueBarcodeHandler extends BarcodeHandler {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
|
||||
barcodeFailureTone();
|
||||
|
||||
@ -435,7 +564,7 @@ class UniqueBarcodeHandler extends BarcodeHandler {
|
||||
}
|
||||
|
||||
@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 (!data.containsKey("hash")) {
|
||||
@ -459,7 +588,9 @@ class UniqueBarcodeHandler extends BarcodeHandler {
|
||||
barcodeSuccessTone();
|
||||
|
||||
// Close the barcode scanner
|
||||
Navigator.of(context).pop();
|
||||
if (OneContext.hasContext) {
|
||||
OneContext().pop();
|
||||
}
|
||||
|
||||
callback(hash);
|
||||
}
|
||||
@ -521,7 +652,7 @@ class _QRViewState extends State<InvenTreeQRView> {
|
||||
_controller?.pauseCamera();
|
||||
|
||||
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";
|
||||
|
||||
|
||||
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
|
||||
*/
|
||||
void debug(dynamic msg) {
|
||||
|
||||
if (Platform.environment.containsKey("FLUTTER_TEST")) {
|
||||
debug_messages.add(msg.toString());
|
||||
print("DEBUG: ${msg.toString()}");
|
||||
}
|
||||
}
|
||||
@ -38,11 +63,13 @@ String simpleNumberString(double number) {
|
||||
*/
|
||||
Future<void> playAudioFile(String path) async {
|
||||
|
||||
// Debug message for unit testing
|
||||
debug("Playing audio file: '${path}'");
|
||||
|
||||
if (!OneContext.hasContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
final player = AudioCache();
|
||||
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 {
|
||||
|
||||
if (fields.isEmpty) {
|
||||
@ -317,7 +320,7 @@ class InvenTreeModel {
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
@ -325,17 +328,13 @@ class InvenTreeModel {
|
||||
url += "/";
|
||||
}
|
||||
|
||||
var response = await api.patch(
|
||||
final response = await api.patch(
|
||||
url,
|
||||
body: values,
|
||||
expectedStatusCode: 200
|
||||
expectedStatusCode: expectedStatusCode,
|
||||
);
|
||||
|
||||
if (!response.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return response;
|
||||
}
|
||||
|
||||
// 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 parentpathstring {
|
||||
// TODO - Drive the refactor tractor through this
|
||||
String get parentPathString {
|
||||
|
||||
List<String> psplit = pathstring.split("/");
|
||||
|
||||
if (psplit.isNotEmpty) {
|
||||
|
@ -535,7 +535,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
* - Remove
|
||||
* - 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")
|
||||
if (isSerialized() && location == null) {
|
||||
@ -566,34 +566,33 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
var response = await api.post(
|
||||
endpoint,
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -602,7 +601,6 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
}
|
||||
|
||||
final bool result = await adjustStock(
|
||||
context,
|
||||
"/stock/transfer/",
|
||||
q,
|
||||
notes: notes,
|
||||
@ -653,12 +651,14 @@ class InvenTreeStockLocation extends InvenTreeModel {
|
||||
return {
|
||||
"name": {},
|
||||
"description": {},
|
||||
"parent": {},
|
||||
"parent": {
|
||||
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
String get parentpathstring {
|
||||
// TODO - Drive the refactor tractor through this
|
||||
String get parentPathString {
|
||||
|
||||
List<String> psplit = pathstring.split("/");
|
||||
|
||||
if (psplit.isNotEmpty) {
|
||||
|
@ -82,6 +82,9 @@
|
||||
"barcodeAssign": "Assign Barcode",
|
||||
"@barcodeAssign": {},
|
||||
|
||||
"barcodeAssignDetail": "Scan custom barcode to assign",
|
||||
"@barcodeAssignDetail": {},
|
||||
|
||||
"barcodeAssigned": "Barcode assigned",
|
||||
"@barcodeAssigned": {},
|
||||
|
||||
@ -106,7 +109,7 @@
|
||||
"barcodeScanGeneral": "Scan an InvenTree barcode",
|
||||
"@barcodeScanGeneral": {},
|
||||
|
||||
"barcodeScanInItems": "Scan stock items into location",
|
||||
"barcodeScanInItems": "Scan stock items into this location",
|
||||
"@barcodeScanInItems": {},
|
||||
|
||||
"barcodeScanLocation": "Scan stock location",
|
||||
@ -807,6 +810,9 @@
|
||||
"scanIntoLocation": "Scan Into Location",
|
||||
"@scanIntoLocation": {},
|
||||
|
||||
"scanIntoLocationDetail": "Scan this item into location",
|
||||
"@scanIntoLocationDetail": {},
|
||||
|
||||
"search": "Search",
|
||||
"@search": {
|
||||
"description": "search"
|
||||
@ -1087,6 +1093,15 @@
|
||||
"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": {},
|
||||
|
||||
|
@ -57,6 +57,9 @@ class UserProfile {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Class for storing and managing user (server) profiles
|
||||
*/
|
||||
class UserProfileDBManager {
|
||||
|
||||
final store = StoreRef("profiles");
|
||||
@ -96,6 +99,8 @@ class UserProfileDBManager {
|
||||
if (exists) {
|
||||
debug("addProfile() : UserProfile '${profile.name}' already exists");
|
||||
return false;
|
||||
} else {
|
||||
debug("Adding new profile: '${profile.name}'");
|
||||
}
|
||||
|
||||
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++) {
|
||||
|
||||
debug("- Checking ${idx} - key = ${profiles[idx].key} - ${profiles[idx].value.toString()}");
|
||||
|
||||
if (profiles[idx].key is int && profiles[idx].key == selected) {
|
||||
return UserProfile.fromJson(
|
||||
profiles[idx].key as int,
|
||||
@ -190,6 +193,24 @@ class UserProfileDBManager {
|
||||
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
|
||||
*/
|
||||
|
@ -131,7 +131,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
children.add(
|
||||
ListTile(
|
||||
title: Text(L10().parentCategory),
|
||||
subtitle: Text("${category?.parentpathstring}"),
|
||||
subtitle: Text("${category?.parentPathString}"),
|
||||
leading: FaIcon(
|
||||
FontAwesomeIcons.levelUpAlt,
|
||||
color: COLOR_CLICK,
|
||||
|
@ -3,11 +3,15 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/preferences.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 {
|
||||
|
||||
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(
|
||||
builder: (context) => SimpleDialog(
|
||||
title: ListTile(
|
||||
title: Text(_error),
|
||||
title: Text(title),
|
||||
leading: FaIcon(icon),
|
||||
),
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(title),
|
||||
subtitle: Text(description),
|
||||
)
|
||||
],
|
||||
children: children
|
||||
)
|
||||
).then((value) {
|
||||
if (onDismissed != null) {
|
||||
@ -106,9 +174,8 @@ Future<void> showServerError(String url, String title, String description) async
|
||||
actionText: L10().details,
|
||||
onAction: () {
|
||||
showErrorDialog(
|
||||
title,
|
||||
description,
|
||||
error: L10().serverError,
|
||||
L10().serverError,
|
||||
description: description,
|
||||
icon: FontAwesomeIcons.server
|
||||
);
|
||||
}
|
||||
|
@ -40,27 +40,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
|
||||
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) {
|
||||
|
||||
// Add "locate" button
|
||||
@ -252,7 +231,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
children.add(
|
||||
ListTile(
|
||||
title: Text(L10().parentLocation),
|
||||
subtitle: Text("${location!.parentpathstring}"),
|
||||
subtitle: Text("${location!.parentPathString}"),
|
||||
leading: FaIcon(FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK),
|
||||
onTap: () {
|
||||
|
||||
@ -381,6 +360,7 @@ List<Widget> detailTiles() {
|
||||
title: Text(L10().locationCreate),
|
||||
subtitle: Text(L10().locationCreateDetail),
|
||||
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
|
||||
trailing: FaIcon(FontAwesomeIcons.plusCircle, color: COLOR_CLICK),
|
||||
onTap: () async {
|
||||
_newLocation(context);
|
||||
},
|
||||
@ -392,6 +372,7 @@ List<Widget> detailTiles() {
|
||||
title: Text(L10().stockItemCreate),
|
||||
subtitle: Text(L10().stockItemCreateDetail),
|
||||
leading: FaIcon(FontAwesomeIcons.boxes, color: COLOR_CLICK),
|
||||
trailing: FaIcon(FontAwesomeIcons.plusCircle, color: COLOR_CLICK),
|
||||
onTap: () async {
|
||||
_newStockItem(context);
|
||||
},
|
||||
@ -401,14 +382,15 @@ List<Widget> detailTiles() {
|
||||
}
|
||||
|
||||
if (location != null) {
|
||||
// Stock adjustment actions
|
||||
|
||||
// Scan stock item into location
|
||||
if (InvenTreeAPI().checkPermission("stock", "change")) {
|
||||
// Scan items into location
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().barcodeScanInItems),
|
||||
title: Text(L10().barcodeScanItem),
|
||||
subtitle: Text(L10().barcodeScanInItems),
|
||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||
trailing: Icon(Icons.qr_code),
|
||||
trailing: Icon(Icons.qr_code, color: COLOR_CLICK),
|
||||
onTap: () {
|
||||
|
||||
var _loc = location;
|
||||
@ -426,21 +408,35 @@ List<Widget> detailTiles() {
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// Scan this location into another one
|
||||
if (InvenTreeAPI().checkPermission("stock_location", "change")) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().transferStockLocation),
|
||||
subtitle: Text(L10().transferStockLocationDetail),
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move location into another location
|
||||
// TODO: Implement this!
|
||||
/*
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text("Move Stock Location"),
|
||||
leading: FaIcon(FontAwesomeIcons.sitemap),
|
||||
trailing: Icon(Icons.qr_code),
|
||||
)
|
||||
);
|
||||
*/
|
||||
|
||||
if (tiles.length <= 1) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
|
@ -381,7 +381,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
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) {
|
||||
|
||||
Map<String, dynamic> fields = {
|
||||
|
@ -1,16 +1,20 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.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
|
||||
*/
|
||||
void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) {
|
||||
|
||||
debug("showSnackIcon: '${text}'");
|
||||
|
||||
// Escape quickly if we do not have context
|
||||
if (!OneContext.hasContext) {
|
||||
// Debug message for unit testing
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -402,20 +402,23 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
|
||||
Future<void> _unassignBarcode(BuildContext context) async {
|
||||
|
||||
final bool result = await item.update(values: {"uid": ""});
|
||||
final response = await item.update(values: {"uid": ""});
|
||||
|
||||
if (result) {
|
||||
showSnackIcon(
|
||||
L10().stockItemUpdateSuccess,
|
||||
success: true
|
||||
);
|
||||
} else {
|
||||
showSnackIcon(
|
||||
L10().stockItemUpdateFailure,
|
||||
success: false,
|
||||
);
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
case 201:
|
||||
showSnackIcon(
|
||||
L10().stockItemUpdateSuccess,
|
||||
success: true
|
||||
);
|
||||
break;
|
||||
default:
|
||||
showSnackIcon(
|
||||
L10().stockItemUpdateFailure,
|
||||
success: false,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
refresh(context);
|
||||
}
|
||||
|
||||
@ -779,6 +782,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().transferStock),
|
||||
subtitle: Text(L10().transferStockDetail),
|
||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||
onTap: () { _transferStockDialog(context); },
|
||||
)
|
||||
@ -788,6 +792,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().scanIntoLocation),
|
||||
subtitle: Text(L10().scanIntoLocationDetail),
|
||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||
trailing: Icon(Icons.qr_code_scanner),
|
||||
onTap: () {
|
||||
@ -806,6 +811,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().barcodeAssign),
|
||||
subtitle: Text(L10().barcodeAssignDetail),
|
||||
leading: Icon(Icons.qr_code),
|
||||
trailing: Icon(Icons.qr_code_scanner),
|
||||
onTap: () {
|
||||
@ -815,17 +821,23 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
values: {
|
||||
"uid": hash,
|
||||
}
|
||||
).then((result) {
|
||||
if (result) {
|
||||
barcodeSuccessTone();
|
||||
).then((response) {
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeAssigned,
|
||||
success: true,
|
||||
icon: Icons.qr_code,
|
||||
);
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
case 201:
|
||||
barcodeSuccessTone();
|
||||
|
||||
refresh(context);
|
||||
showSnackIcon(
|
||||
L10().barcodeAssigned,
|
||||
success: true,
|
||||
icon: Icons.qr_code,
|
||||
);
|
||||
|
||||
refresh(context);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -59,7 +59,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
||||
Map<String, String> get orderingOptions => {
|
||||
"part__name": L10().name,
|
||||
"part__IPN": L10().internalPartNumber,
|
||||
"quantity": L10().quantity,
|
||||
"stock": L10().quantity,
|
||||
"status": L10().status,
|
||||
"batch": L10().batchCode,
|
||||
"updated": L10().lastUpdated,
|
||||
|
@ -5,6 +5,7 @@
|
||||
import "package:test/test.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/user_profile.dart";
|
||||
|
||||
|
||||
@ -94,6 +95,9 @@ void main() {
|
||||
|
||||
assert(!api.checkConnection());
|
||||
|
||||
debugContains("Token request failed: STATUS 401");
|
||||
debugContains("showSnackIcon: 'Not Connected'");
|
||||
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
@ -137,6 +141,9 @@ void main() {
|
||||
assert(api.checkPermission("stocklocation", "delete"));
|
||||
assert(!api.checkPermission("part", "weirdpermission"));
|
||||
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
|
||||
*/
|
||||
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:test/test.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/user_profile.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
|
||||
|
||||
@ -112,32 +112,40 @@ void main() {
|
||||
assert(result != null);
|
||||
assert(result is InvenTreePart);
|
||||
|
||||
APIResponse? response;
|
||||
|
||||
if (result != null) {
|
||||
InvenTreePart part = result as InvenTreePart;
|
||||
assert(part.name == "M2x4 LPHS");
|
||||
|
||||
// Change the name to something else
|
||||
assert(await part.update(
|
||||
|
||||
response = await part.update(
|
||||
values: {
|
||||
"name": "Woogle",
|
||||
}
|
||||
));
|
||||
);
|
||||
|
||||
assert(response.isValid());
|
||||
assert(response.statusCode == 200);
|
||||
|
||||
assert(await part.reload());
|
||||
assert(part.name == "Woogle");
|
||||
|
||||
// And change it back again
|
||||
assert(await part.update(
|
||||
response = await part.update(
|
||||
values: {
|
||||
"name": "M2x4 LPHS"
|
||||
}
|
||||
));
|
||||
);
|
||||
|
||||
assert(response.isValid());
|
||||
assert(response.statusCode == 200);
|
||||
|
||||
assert(await part.reload());
|
||||
assert(part.name == "M2x4 LPHS");
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user