2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-28 05:26:47 +00:00

Merge branch 'barcode-actions'

This commit is contained in:
Oliver Walters 2021-01-29 00:59:55 +11:00
commit 13780b0c80
7 changed files with 305 additions and 94 deletions

View File

@ -41,6 +41,16 @@ class BarcodeHandler {
Future<void> onBarcodeUnknown(Map<String, dynamic> data) {
// Called when the server does not know about a barcode
// Override this function
showErrorDialog(
_context,
"Invalid Barcode",
"Barcode does not match any known item",
error: "Barcode Error",
icon: FontAwesomeIcons.barcode,
onDismissed: () {
_controller.resumeCamera();
}
);
}
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) {
@ -339,6 +349,40 @@ class _QRViewState extends State<InvenTreeQRView> {
}
class StockItemScanIntoLocationHandler extends BarcodeHandler {
/**
* Barcode handler for scanning a provided StockItem into a scanned StockLocation
*/
final InvenTreeStockItem item;
StockItemScanIntoLocationHandler(this.item);
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) {
// If the barcode points to a 'stocklocation', great!
if (!data.containsKey('stocklocation')) {
showErrorDialog(
_context,
"Invalid Barcode",
"Barcode does not match a Stock Location",
onDismissed: _controller.resumeCamera,
);
} else {
// Extract location information
int location = data['stocklocation']['pk'] as int;
// Transfer stock to specified location
item.transferStock(location).then((response) {
print("Response: ${response.statusCode}");
_controller.dispose();
Navigator.of(_context).pop();
});
}
}
}
Future<void> scanQrCode(BuildContext context) async {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler())));

View File

@ -379,20 +379,26 @@ class InvenTreeStockItem extends InvenTreeModel {
});
}
Future<http.Response> transferStock(double q, int location, {String notes}) async {
Future<http.Response> transferStock(int location, {double quantity, String notes}) async {
if (quantity == null) {} else
if ((quantity < 0) || (quantity > this.quantity)) {
quantity = this.quantity;
}
if ((q == null) || (q > quantity)) q = quantity;
return api.post("/stock/transfer/", body: {
Map<String, dynamic> data = {
"item": {
"pk": "${pk}",
"quantity": "${q}",
},
},
"location": "${location}",
"notes": notes ?? '',
});
}
};
if (quantity != null) {
data["item"]["quantity"] = "${quantity}";
}
return api.post("/stock/transfer/", body: data);
}
}

View File

@ -141,7 +141,6 @@ class QuantityField extends TextFormField {
labelText: label,
hintText: hint,
),
initialValue: initial,
controller: controller,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
validator: (value) {

View File

@ -133,11 +133,87 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
}
@override
Widget getBody(BuildContext context) {
Widget getBottomNavBar(BuildContext context) {
return BottomNavigationBar(
currentIndex: tabIndex,
onTap: onTabSelectionChanged,
items: const <BottomNavigationBarItem> [
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.boxes),
title: Text("Stock"),
),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.wrench),
title: Text("Actions"),
)
]
);
}
return ListView(
children: <Widget> [
locationDescriptionCard(),
Widget getSelectedWidget(int index) {
switch (index) {
case 0:
return ListView(
children: detailTiles(),
);
case 1:
return ListView(
children: actionTiles(),
);
default:
return null;
}
}
@override
Widget getBody(BuildContext context) {
return getSelectedWidget(tabIndex);
}
List<Widget> detailTiles() {
List<Widget> tiles = [];
// Location description
tiles.add(locationDescriptionCard());
// Sublocation panel
ExpansionPanel sublocations = ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text("Sublocations"),
leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
trailing: Text("${_sublocations.length}"),
onTap: () {
setState(() {
InvenTreePreferences().expandLocationList = !InvenTreePreferences().expandLocationList;
});
},
);
},
body: SublocationList(_sublocations),
isExpanded: InvenTreePreferences().expandLocationList && _sublocations.length > 0,
);
ExpansionPanel subitems = ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text("Stock Items"),
leading: FaIcon(FontAwesomeIcons.boxes),
trailing: Text("${_items.length}"),
onTap: () {
setState(() {
InvenTreePreferences().expandStockList = !InvenTreePreferences().expandStockList;
});
},
);
},
body: StockList(_items),
isExpanded: InvenTreePreferences().expandStockList && _items.length > 0,
);
// Sublocations and items
tiles.add(
ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
@ -152,49 +228,48 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
break;
}
});
},
children: <ExpansionPanel> [
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text("Sublocations"),
leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
trailing: Text("${_sublocations.length}"),
onTap: () {
setState(() {
InvenTreePreferences().expandLocationList = !InvenTreePreferences().expandLocationList;
});
},
);
},
body: SublocationList(_sublocations),
isExpanded: InvenTreePreferences().expandLocationList && _sublocations.length > 0,
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text("Stock Items"),
leading: FaIcon(FontAwesomeIcons.boxes),
trailing: Text("${_items.length}"),
onTap: () {
setState(() {
InvenTreePreferences().expandStockList = !InvenTreePreferences().expandStockList;
});
},
);
},
body: StockList(_items),
isExpanded: InvenTreePreferences().expandStockList && _items.length > 0,
)
sublocations,
subitems,
]
),
]
)
);
return tiles;
}
List<Widget> actionTiles() {
List<Widget> tiles = [];
tiles.add(locationDescriptionCard());
// Scan items into location
tiles.add(
ListTile(
title: Text("Scan in Stock Item"),
leading: FaIcon(FontAwesomeIcons.exchangeAlt),
trailing: FaIcon(FontAwesomeIcons.qrcode),
onTap: null,
)
);
// Move location into another location
tiles.add(
ListTile(
title: Text("Move Stock Location"),
leading: FaIcon(FontAwesomeIcons.sitemap),
trailing: FaIcon(FontAwesomeIcons.qrcode),
)
);
return tiles;
}
}
class SublocationList extends StatelessWidget {
final List<InvenTreeStockLocation> _locations;

View File

@ -51,8 +51,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
InvenTreePart part;
int _tabIndex = 0;
@override
Future<void> onBuild(BuildContext context) async {
refresh();
@ -302,12 +300,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
}
void _onTabSelectionChanged(int index) {
setState(() {
_tabIndex = index;
});
}
Widget getSelectedWidget(int index) {
switch (index) {
case 0:
@ -335,8 +327,8 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
@override
Widget getBottomNavBar(BuildContext context) {
return BottomNavigationBar(
currentIndex: _tabIndex,
onTap: _onTabSelectionChanged,
currentIndex: tabIndex,
onTap: onTabSelectionChanged,
items: const <BottomNavigationBarItem> [
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.infoCircle),
@ -356,6 +348,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
@override
Widget getBody(BuildContext context) {
return getSelectedWidget(_tabIndex);
return getSelectedWidget(tabIndex);
}
}

View File

@ -11,6 +11,16 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
// Storage for context once "Build" is called
BuildContext context;
// Current tab index (used for widgets which display bottom tabs)
int tabIndex = 0;
// Update current tab selection
void onTabSelectionChanged(int index) {
setState(() {
tabIndex = index;
});
}
List<Widget> getAppBarActions(BuildContext context) {
return [];
}

View File

@ -119,6 +119,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
void _addStockDialog() async {
_quantityController.clear();
_notesController.clear();
showFormDialog(context, "Add Stock",
key: _addStockKey,
@ -165,6 +166,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
void _removeStockDialog() {
_quantityController.clear();
_notesController.clear();
showFormDialog(context, "Remove Stock",
key: _removeStockKey,
@ -212,6 +214,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
void _countStockDialog() async {
_quantityController.text = item.quantity.toString();
_notesController.clear();
showFormDialog(context, "Count Stock",
key: _countStockKey,
actions: <Widget> [
@ -248,7 +253,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
_quantityController.clear();
_notesController.clear();
var response = await item.transferStock(quantity, location.pk, notes: notes);
var response = await item.transferStock(location.pk, quantity: quantity, notes: notes);
// TODO - Error handling (potentially return false?)
refresh();
@ -331,23 +336,25 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
);
}
Widget headerTile() {
return Card(
child: ListTile(
title: Text("${item.partName}"),
subtitle: Text("${item.partDescription}"),
leading: InvenTreeAPI().getImage(item.partImage),
)
);
}
/*
* Construct a list of detail elements about this StockItem.
* The number of elements may vary depending on the StockItem details
*/
List<Widget> stockTiles() {
List<Widget> detailTiles() {
List<Widget> tiles = [];
// Image / name / description
tiles.add(
Card(
child: ListTile(
title: Text("${item.partName}"),
subtitle: Text("${item.partDescription}"),
leading: InvenTreeAPI().getImage(item.partImage),
)
)
);
tiles.add(headerTile());
tiles.add(
ListTile(
@ -412,20 +419,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
);
}
tiles.add(
ListTile(
title: Text("Add Barcode"),
leading: FaIcon(FontAwesomeIcons.qrcode),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemBarcodeAssignmentHandler(item)))
);
//Navigator.push(context, MaterialPageRoute(builder: (context) => AssignBarcodeToStockItemView(item)));
},
)
);
// Supplier part?
if (item.supplierPartId > 0) {
tiles.add(
@ -486,11 +479,87 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
return tiles;
}
List<Widget> actionTiles() {
List<Widget> tiles = [];
tiles.add(headerTile());
if (!item.isSerialized()) {
tiles.add(
ListTile(
title: Text("Count Stock"),
leading: FaIcon(FontAwesomeIcons.checkCircle),
onTap: _countStockDialog,
)
);
tiles.add(
ListTile(
title: Text("Remove Stock"),
leading: FaIcon(FontAwesomeIcons.minusCircle),
onTap: _removeStockDialog,
)
);
tiles.add(
ListTile(
title: Text("Add Stock"),
leading: FaIcon(FontAwesomeIcons.plusCircle),
onTap: _addStockDialog,
)
);
}
tiles.add(
ListTile(
title: Text("Transfer Stock"),
leading: FaIcon(FontAwesomeIcons.exchangeAlt),
onTap: _transferStockDialog,
)
);
// Scan item into a location
tiles.add(
ListTile(
title: Text("Scan Into Location"),
leading: FaIcon(FontAwesomeIcons.exchangeAlt),
trailing: FaIcon(FontAwesomeIcons.qrcode),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemScanIntoLocationHandler(item)))
);
},
)
);
// Add or remove custom barcode
if (item.uid.isEmpty) {
tiles.add(
ListTile(
title: Text("Assign Barcode"),
leading: FaIcon(FontAwesomeIcons.barcode),
trailing: FaIcon(FontAwesomeIcons.qrcode),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemBarcodeAssignmentHandler(item)))
);
}
)
);
}
return tiles;
}
/*
* Return a list of context-sensitive action buttons.
* Not all buttons will be avaialable for a given StockItem,
* depending on the properties of that StockItem
*/
/*
List<SpeedDialChild> actionButtons() {
var buttons = List<SpeedDialChild>();
@ -525,32 +594,47 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
return buttons;
}
*/
@override
Widget getBottomNavBar(BuildContext context) {
return BottomNavigationBar(
currentIndex: 0,
onTap: null,
currentIndex: tabIndex,
onTap: onTabSelectionChanged,
items: const <BottomNavigationBarItem> [
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.infoCircle),
title: Text("Details"),
),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.history),
title: Text("History"),
)
icon: FaIcon(FontAwesomeIcons.wrench),
title: Text("Actions"),
),
]
);
}
@override
Widget getBody(BuildContext context) {
return ListView(
children: stockTiles()
);
Widget getSelectedWidget(int index) {
switch (index) {
case 0:
return ListView(
children: detailTiles(),
);
case 1:
return ListView(
children: actionTiles(),
);
default:
return null;
}
}
@override
Widget getBody(BuildContext context) {
return getSelectedWidget(tabIndex);
}
/*
@override
Widget getFab(BuildContext context) {
return SpeedDial(
@ -560,4 +644,5 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
children: actionButtons(),
);
}
*/
}