2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-06-13 10:45:29 +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:
Oliver
2022-07-18 22:10:00 +10:00
committed by GitHub
parent c6678e201f
commit aa274b2e45
21 changed files with 711 additions and 255 deletions

View File

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

View File

@ -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
);
}

View File

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

View File

@ -381,7 +381,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
tiles.add(
ListTile(
title: Text("${part.keywords}"),
leading: FaIcon(FontAwesomeIcons.key),
leading: FaIcon(FontAwesomeIcons.tags),
)
);
}

View File

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

View File

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

View File

@ -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;
}
});
});

View File

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