From db6aae8a783720a71f29edfe63d067d3c3c93149 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Jan 2021 00:08:17 +1100 Subject: [PATCH 1/3] Add "actions" panel to stock item view --- lib/widget/fields.dart | 1 - lib/widget/part_detail.dart | 14 +-- lib/widget/refreshable_state.dart | 10 ++ lib/widget/stock_detail.dart | 149 +++++++++++++++++++++++------- 4 files changed, 128 insertions(+), 46 deletions(-) diff --git a/lib/widget/fields.dart b/lib/widget/fields.dart index 80718bc5..c8cba169 100644 --- a/lib/widget/fields.dart +++ b/lib/widget/fields.dart @@ -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) { diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index a06da60a..5d23eacf 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -51,8 +51,6 @@ class _PartDisplayState extends RefreshableState { InvenTreePart part; - int _tabIndex = 0; - @override Future onBuild(BuildContext context) async { refresh(); @@ -302,12 +300,6 @@ class _PartDisplayState extends RefreshableState { } - void _onTabSelectionChanged(int index) { - setState(() { - _tabIndex = index; - }); - } - Widget getSelectedWidget(int index) { switch (index) { case 0: @@ -335,8 +327,8 @@ class _PartDisplayState extends RefreshableState { @override Widget getBottomNavBar(BuildContext context) { return BottomNavigationBar( - currentIndex: _tabIndex, - onTap: _onTabSelectionChanged, + currentIndex: tabIndex, + onTap: onTabSelectionChanged, items: const [ BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.infoCircle), @@ -356,6 +348,6 @@ class _PartDisplayState extends RefreshableState { @override Widget getBody(BuildContext context) { - return getSelectedWidget(_tabIndex); + return getSelectedWidget(tabIndex); } } \ No newline at end of file diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart index 44e48553..84cf35ab 100644 --- a/lib/widget/refreshable_state.dart +++ b/lib/widget/refreshable_state.dart @@ -11,6 +11,16 @@ abstract class RefreshableState extends State { // 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 getAppBarActions(BuildContext context) { return []; } diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index d08b89cc..c769788b 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -119,6 +119,7 @@ class _StockItemDisplayState extends RefreshableState { void _addStockDialog() async { _quantityController.clear(); + _notesController.clear(); showFormDialog(context, "Add Stock", key: _addStockKey, @@ -165,6 +166,7 @@ class _StockItemDisplayState extends RefreshableState { void _removeStockDialog() { _quantityController.clear(); + _notesController.clear(); showFormDialog(context, "Remove Stock", key: _removeStockKey, @@ -212,6 +214,9 @@ class _StockItemDisplayState extends RefreshableState { void _countStockDialog() async { + _quantityController.text = item.quantity.toString(); + _notesController.clear(); + showFormDialog(context, "Count Stock", key: _countStockKey, actions: [ @@ -331,23 +336,25 @@ class _StockItemDisplayState extends RefreshableState { ); } + 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 stockTiles() { + List detailTiles() { List 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 { ); } - 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,83 @@ class _StockItemDisplayState extends RefreshableState { return tiles; } + List actionTiles() { + List 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: () { + }, + ) + ); + + // 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 actionButtons() { var buttons = List(); @@ -525,32 +590,47 @@ class _StockItemDisplayState extends RefreshableState { return buttons; } + */ @override Widget getBottomNavBar(BuildContext context) { return BottomNavigationBar( - currentIndex: 0, - onTap: null, + currentIndex: tabIndex, + onTap: onTabSelectionChanged, items: const [ 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 +640,5 @@ class _StockItemDisplayState extends RefreshableState { children: actionButtons(), ); } + */ } \ No newline at end of file From c00e367ae56b0c75ce0d3c0796b97bd1aa76ac2d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Jan 2021 00:28:50 +1100 Subject: [PATCH 2/3] Add barcode handler to scan stock item into location --- lib/barcode.dart | 34 ++++++++++++++++++++++++++++++++++ lib/inventree/stock.dart | 22 ++++++++++++++-------- lib/widget/stock_detail.dart | 6 +++++- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/lib/barcode.dart b/lib/barcode.dart index ce03cce8..2beaad2f 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -339,6 +339,40 @@ class _QRViewState extends State { } +class StockItemScanIntoLocationHandler extends BarcodeHandler { + /** + * Barcode handler for scanning a provided StockItem into a scanned StockLocation + */ + + final InvenTreeStockItem item; + + StockItemScanIntoLocationHandler(this.item); + + @override + Future onBarcodeMatched(Map 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 scanQrCode(BuildContext context) async { Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler()))); diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 1899cb48..c174e048 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -379,20 +379,26 @@ class InvenTreeStockItem extends InvenTreeModel { }); } - Future transferStock(double q, int location, {String notes}) async { + Future 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 data = { "item": { "pk": "${pk}", - "quantity": "${q}", - }, + }, "location": "${location}", "notes": notes ?? '', - }); - } + }; + if (quantity != null) { + data["item"]["quantity"] = "${quantity}"; + } + + return api.post("/stock/transfer/", body: data); + } } diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index c769788b..46b83764 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -253,7 +253,7 @@ class _StockItemDisplayState extends RefreshableState { _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(); @@ -525,6 +525,10 @@ class _StockItemDisplayState extends RefreshableState { leading: FaIcon(FontAwesomeIcons.exchangeAlt), trailing: FaIcon(FontAwesomeIcons.qrcode), onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemScanIntoLocationHandler(item))) + ); }, ) ); From 3433d57f8387d5379a81c958476442a4e28bcc85 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Jan 2021 00:52:15 +1100 Subject: [PATCH 3/3] Refactor display for StockLocation --- lib/barcode.dart | 10 ++ lib/widget/location_display.dart | 153 +++++++++++++++++++++++-------- 2 files changed, 124 insertions(+), 39 deletions(-) diff --git a/lib/barcode.dart b/lib/barcode.dart index 2beaad2f..f92c9ae7 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -41,6 +41,16 @@ class BarcodeHandler { Future onBarcodeUnknown(Map 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 onBarcodeUnhandled(Map data) { diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index d61c8a70..49223c14 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -133,11 +133,87 @@ class _LocationDisplayState extends RefreshableState { } @override - Widget getBody(BuildContext context) { + Widget getBottomNavBar(BuildContext context) { + return BottomNavigationBar( + currentIndex: tabIndex, + onTap: onTabSelectionChanged, + items: const [ + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.boxes), + title: Text("Stock"), + ), + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.wrench), + title: Text("Actions"), + ) + ] + ); + } - return ListView( - children: [ - 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 detailTiles() { + List 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 { break; } }); - }, children: [ - 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 actionTiles() { + List 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 _locations;