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:
@ -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,
|
||||
|
Reference in New Issue
Block a user