From 359b5d9b4ae0ce6121ff17f29c3aecb55f777b11 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 7 Apr 2020 11:12:45 +1000 Subject: [PATCH 01/17] Add floating-action button to StockDetail view --- lib/widget/stock_detail.dart | 5 +++++ pubspec.lock | 2 +- pubspec.yaml | 14 +++++--------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 507cb72a..feb93548 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -159,6 +159,11 @@ class _StockItemDisplayState extends State { title: Text("Stock Item"), ), drawer: new InvenTreeDrawer(context), + floatingActionButton: FloatingActionButton( + child: FaIcon(FontAwesomeIcons.ellipsisH), + // TODO: Add pop-up icons + // REF: https://stackoverflow.com/questions/46480221/flutter-floating-action-button-with-speed-dial#46480722 + ), body: Center( child: ListView( children: stockTiles(), diff --git a/pubspec.lock b/pubspec.lock index dde8775c..da7e2374 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -225,7 +225,7 @@ packages: name: preferences url: "https://pub.dartlang.org" source: hosted - version: "5.1.0" + version: "5.2.0" qr_utils: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2a381194..175e6ae7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,15 +26,11 @@ dependencies: http: ^0.12.0+2 shared_preferences: ^0.5.3+1 - flutter_advanced_networkimage: any - - preferences: ^5.1.0 - - qr_utils: ^0.1.4 - - package_info: ^0.4.0+16 - - font_awesome_flutter: ^8.8.1 + flutter_advanced_networkimage: any # Pull image from network or cache + preferences: ^5.1.0 # Persistent settings storage + qr_utils: ^0.1.4 # Barcode / QR-code support + package_info: ^0.4.0+16 # App information introspection + font_awesome_flutter: ^8.8.1 # FontAwesome icon set dev_dependencies: flutter_test: From 0cc2efe218b1eb58bdc82154f04f34d199733712 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 7 Apr 2020 11:13:51 +1000 Subject: [PATCH 02/17] Add FloatingActionButton to PartDetail page --- lib/widget/part_detail.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 13a7d498..e207db40 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -160,6 +160,11 @@ class _PartDisplayState extends State { title: Text("Part Details"), ), drawer: new InvenTreeDrawer(context), + floatingActionButton: FloatingActionButton( + child: FaIcon(FontAwesomeIcons.ellipsisH), + // TODO - Add pop-up icons + // Ref: https://stackoverflow.com/questions/46480221/flutter-floating-action-button-with-speed-dial#46480722 + ), body: Center( child: ListView( children: partTiles(), From 5382ded50c116dc61d70084d30a755ec4153c6b8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 8 Apr 2020 11:59:39 +1000 Subject: [PATCH 03/17] Add FAB context menu for the StockItem view - Uses flutter_speed_dial plugin --- lib/widget/stock_detail.dart | 47 +++++++++++++++++++++++++++++++++--- pubspec.lock | 7 ++++++ pubspec.yaml | 1 + 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index feb93548..ab4a7225 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -11,6 +11,7 @@ import 'package:InvenTree/api.dart'; import 'package:InvenTree/widget/drawer.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:flutter_speed_dial/flutter_speed_dial.dart'; class StockDetailWidget extends StatefulWidget { @@ -152,6 +153,43 @@ class _StockItemDisplayState extends State { 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(); + + buttons.add(SpeedDialChild( + child: Icon(Icons.add_circle), + label: "Add Stock", + onTap: null, + ) + ); + + buttons.add(SpeedDialChild( + child: Icon(Icons.remove_circle), + label: "Remove Stock", + onTap: null, + ), + ); + + buttons.add(SpeedDialChild( + child: Icon(Icons.check_circle), + label: "Count Stock", + onTap: null, + )); + + buttons.add(SpeedDialChild( + child: Icon(Icons.location_on), + label: "Transfer Stock", + onTap: null, + )); + + return buttons; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -159,10 +197,11 @@ class _StockItemDisplayState extends State { title: Text("Stock Item"), ), drawer: new InvenTreeDrawer(context), - floatingActionButton: FloatingActionButton( - child: FaIcon(FontAwesomeIcons.ellipsisH), - // TODO: Add pop-up icons - // REF: https://stackoverflow.com/questions/46480221/flutter-floating-action-button-with-speed-dial#46480722 + floatingActionButton: SpeedDial( + visible: true, + animatedIcon: AnimatedIcons.menu_close, + heroTag: 'stock-item-fab', + children: actionButtons(), ), body: Center( child: ListView( diff --git a/pubspec.lock b/pubspec.lock index da7e2374..fa4c5ec7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -83,6 +83,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.4" + flutter_speed_dial: + dependency: "direct main" + description: + name: flutter_speed_dial + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.5" flutter_svg: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 175e6ae7..b34b622b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: qr_utils: ^0.1.4 # Barcode / QR-code support package_info: ^0.4.0+16 # App information introspection font_awesome_flutter: ^8.8.1 # FontAwesome icon set + flutter_speed_dial: ^1.2.5 # FAB menu elements dev_dependencies: flutter_test: From 04b09b55911bb90c11d5e7546df2a1c1fb562c7f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 8 Apr 2020 12:18:50 +1000 Subject: [PATCH 04/17] Display either serial number or quantity depending on stock properties --- lib/inventree/stock.dart | 2 ++ lib/widget/stock_detail.dart | 26 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 2365b74b..5336ff33 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -121,6 +121,8 @@ class InvenTreeStockItem extends InvenTreeModel { int get locationId => jsondata['location'] as int ?? -1; + bool isSerialized() => serialNumber != null && quantity.toInt() == 1; + String get locationName { String loc = ''; diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index ab4a7225..b770d89f 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -74,13 +74,25 @@ class _StockItemDisplayState extends State { ); // Quantity information - tiles.add( - ListTile( - title: Text("Quantity"), - leading: FaIcon(FontAwesomeIcons.cubes), - trailing: Text("${item.quantity}"), - ) - ); + if (item.isSerialized()) { + tiles.add( + ListTile( + title: Text("Serial Number"), + leading: FaIcon(FontAwesomeIcons.hashtag), + trailing: Text("${item.serialNumber}"), + ) + ); + } else { + tiles.add( + ListTile( + title: Text("Quantity"), + leading: FaIcon(FontAwesomeIcons.cubes), + trailing: Text("${item.quantity}"), + ) + ); + + } + // Location information if (item.locationName.isNotEmpty) { From a64f46d79c6afb996721aa2fa40366e6d79d6991 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 8 Apr 2020 12:24:32 +1000 Subject: [PATCH 05/17] Selectively show certain buttons in the StockItem FAB menu --- lib/widget/stock_detail.dart | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index b770d89f..407dc5f3 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -173,25 +173,28 @@ class _StockItemDisplayState extends State { List actionButtons() { var buttons = List(); - buttons.add(SpeedDialChild( - child: Icon(Icons.add_circle), - label: "Add Stock", - onTap: null, + // The following actions only apply if the StockItem is not serialized + if (!item.isSerialized()) { + buttons.add(SpeedDialChild( + child: Icon(Icons.add_circle), + label: "Add Stock", + onTap: null, ) - ); + ); - buttons.add(SpeedDialChild( - child: Icon(Icons.remove_circle), - label: "Remove Stock", - onTap: null, + buttons.add(SpeedDialChild( + child: Icon(Icons.remove_circle), + label: "Remove Stock", + onTap: null, ), - ); + ); - buttons.add(SpeedDialChild( - child: Icon(Icons.check_circle), - label: "Count Stock", - onTap: null, - )); + buttons.add(SpeedDialChild( + child: Icon(Icons.check_circle), + label: "Count Stock", + onTap: null, + )); + } buttons.add(SpeedDialChild( child: Icon(Icons.location_on), From 08e23039c08599dd9ba37ff8b82334daf93e4ff6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 9 Apr 2020 08:29:47 +1000 Subject: [PATCH 06/17] Placeholder callback functions --- lib/widget/stock_detail.dart | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 407dc5f3..5155c614 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -32,6 +32,26 @@ class _StockItemDisplayState extends State { final InvenTreeStockItem item; + void _editStockItem() { + // TODO - Form for editing stock item + } + + void _addStock() { + // TODO - Form for adding stock + } + + void _removeStock() { + // TODO - Form for removing stock + } + + void _countStock() { + // TODO - Form for counting stock + } + + void _transferStock() { + // TODO - Form for transferring stock + } + /* * Construct a list of detail elements about this StockItem. * The number of elements may vary depending on the StockItem details @@ -50,7 +70,7 @@ class _StockItemDisplayState extends State { ), trailing: IconButton( icon: FaIcon(FontAwesomeIcons.edit), - onPressed: null, + onPressed: _editStockItem, ) ) ) @@ -178,28 +198,28 @@ class _StockItemDisplayState extends State { buttons.add(SpeedDialChild( child: Icon(Icons.add_circle), label: "Add Stock", - onTap: null, + onTap: _addStock, ) ); buttons.add(SpeedDialChild( child: Icon(Icons.remove_circle), label: "Remove Stock", - onTap: null, + onTap: _removeStock, ), ); buttons.add(SpeedDialChild( child: Icon(Icons.check_circle), label: "Count Stock", - onTap: null, + onTap: _countStock, )); } buttons.add(SpeedDialChild( child: Icon(Icons.location_on), label: "Transfer Stock", - onTap: null, + onTap: _transferStock, )); return buttons; From 1ec0b13479c2d410d3e1600ff7ef2a4268fe3c56 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 9 Apr 2020 08:52:53 +1000 Subject: [PATCH 07/17] Add (empty) search page --- lib/main.dart | 8 +++++--- lib/widget/drawer.dart | 8 +++++++- lib/widget/search.dart | 33 +++++++++++++++++++++++++++++++++ lib/widget/stock_detail.dart | 8 ++++---- 4 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 lib/widget/search.dart diff --git a/lib/main.dart b/lib/main.dart index 2f400069..4dd424d9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/widget/category_display.dart'; import 'package:InvenTree/widget/company_list.dart'; import 'package:InvenTree/widget/location_display.dart'; +import 'package:InvenTree/widget/search.dart'; import 'package:InvenTree/widget/drawer.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -192,7 +193,8 @@ class _MyHomePageState extends State { void _search() { if (!InvenTreeAPI().checkConnection(context)) return; - // TODO + Navigator.push(context, MaterialPageRoute(builder: (context) => SearchWidget())); + } void _scan() { @@ -251,7 +253,7 @@ class _MyHomePageState extends State { IconButton( icon: FaIcon(FontAwesomeIcons.search), tooltip: 'Search', - onPressed: null, + onPressed: _search, ), ], ), @@ -271,7 +273,7 @@ class _MyHomePageState extends State { IconButton( icon: new FaIcon(FontAwesomeIcons.search), tooltip: 'Search', - onPressed: _unsupported, + onPressed: _search, ), Text("Search"), ], diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart index f5db6e59..3a4536df 100644 --- a/lib/widget/drawer.dart +++ b/lib/widget/drawer.dart @@ -1,6 +1,7 @@ import 'package:InvenTree/api.dart'; import 'package:InvenTree/barcode.dart'; import 'package:InvenTree/widget/company_list.dart'; +import 'package:InvenTree/widget/search.dart'; import 'package:flutter/material.dart'; import 'package:InvenTree/api.dart'; @@ -32,6 +33,11 @@ class InvenTreeDrawer extends StatelessWidget { Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false); } + void _search() { + _closeDrawer(); + Navigator.push(context, MaterialPageRoute(builder: (context) => SearchWidget())); + } + /* * Launch the camera to scan a QR code. * Upon successful scan, data are passed off to be decoded. @@ -102,7 +108,7 @@ class InvenTreeDrawer extends StatelessWidget { new ListTile( title: new Text("Search"), leading: new FaIcon(FontAwesomeIcons.search), - onTap: null, + onTap: _search, ), new ListTile( title: new Text("Scan Barcode"), diff --git a/lib/widget/search.dart b/lib/widget/search.dart new file mode 100644 index 00000000..52bf164a --- /dev/null +++ b/lib/widget/search.dart @@ -0,0 +1,33 @@ + +import 'package:InvenTree/widget/drawer.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class SearchWidget extends StatefulWidget { + + @override + _SearchState createState() => _SearchState(); +} + + +class _SearchState extends State { + + @override + Widget build(BuildContext context) { + + return Scaffold( + appBar: AppBar( + title: Text("Search"), + ), + drawer: new InvenTreeDrawer(context), + body: Center( + child: ListView( + children: [ + + ], + ) + ) + ); + + } +} \ No newline at end of file diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 5155c614..3f3787ba 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -196,28 +196,28 @@ class _StockItemDisplayState extends State { // The following actions only apply if the StockItem is not serialized if (!item.isSerialized()) { buttons.add(SpeedDialChild( - child: Icon(Icons.add_circle), + child: Icon(FontAwesomeIcons.plusCircle), label: "Add Stock", onTap: _addStock, ) ); buttons.add(SpeedDialChild( - child: Icon(Icons.remove_circle), + child: Icon(FontAwesomeIcons.minusCircle), label: "Remove Stock", onTap: _removeStock, ), ); buttons.add(SpeedDialChild( - child: Icon(Icons.check_circle), + child: Icon(FontAwesomeIcons.checkCircle), label: "Count Stock", onTap: _countStock, )); } buttons.add(SpeedDialChild( - child: Icon(Icons.location_on), + child: Icon(FontAwesomeIcons.exchangeAlt), label: "Transfer Stock", onTap: _transferStock, )); From 4cafa668a540a0631adefb871d8f70be9c0f99ea Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 9 Apr 2020 22:56:28 +1000 Subject: [PATCH 08/17] Perform "addstock" action --- lib/api.dart | 21 +++++++++------ lib/inventree/model.dart | 4 +-- lib/inventree/stock.dart | 25 +++++++++++++++++- lib/widget/stock_detail.dart | 51 ++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 885fd145..c2872b6b 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -59,7 +59,7 @@ class InvenTreeAPI { String makeApiUrl(String endpoint) { - return apiUrl + endpoint; + return _makeUrl("/api/" + endpoint); } String makeUrl(String endpoint) { @@ -272,20 +272,18 @@ class InvenTreeAPI { } // Perform a POST request - Future post(String url, {Map body}) async { + Future post(String url, {Map body}) async { var _url = makeApiUrl(url); - var _headers = defaultHeaders(); - var _body = Map(); - - // Copy across provided data - body.forEach((K, V) => _body[K] = V); + var _headers = jsonHeaders(); print("POST: " + _url); + var data = jsonEncode(body); + return http.post(_url, headers: _headers, - body: _body, + body: data, ); } @@ -324,6 +322,13 @@ class InvenTreeAPI { return headers; } + Map jsonHeaders() { + + var headers = defaultHeaders(); + headers['Content-Type'] = 'application/json'; + return headers; + } + String _authorizationHeader () { if (_token.isNotEmpty) { return "Token $_token"; diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 7b62ac0e..d929df88 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -102,7 +102,7 @@ class InvenTreeModel { print("GET: $addr ${params.toString()}"); - var response = await InvenTreeAPI().get(addr, params: params); + var response = await api.get(addr, params: params); if (response.statusCode != 200) { print("Error retrieving data"); @@ -134,7 +134,7 @@ class InvenTreeModel { // TODO - Add "timeout" // TODO - Add error catching - var response = await InvenTreeAPI().get(URL, params:params); + var response = await api.get(URL, params:params); // A list of "InvenTreeModel" items List results = new List(); diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 5336ff33..9679ab6d 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:http/http.dart' as http; import 'model.dart'; import 'package:InvenTree/api.dart'; @@ -155,6 +156,29 @@ class InvenTreeStockItem extends InvenTreeModel { return item; } + + Future addStock(double quan) async { + + // Cannot add stock to a serialized StockItem + if (isSerialized()) { + return null; + } + + // Cannot add negative stock + if (quan <= 0) { + return null; + } + + Map data = { + "item": { + "pk": "${pk}", + "quantity": "${quan}", + } + }; + + return api.post("/stock/add/", body: data); + } + } @@ -196,5 +220,4 @@ class InvenTreeStockLocation extends InvenTreeModel { return loc; } - } \ No newline at end of file diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 3f3787ba..cc5f782a 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:InvenTree/api.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; @@ -26,6 +27,8 @@ class StockDetailWidget extends StatefulWidget { class _StockItemDisplayState extends State { + final _addStockKey = GlobalKey(); + _StockItemDisplayState(this.item) { // TODO } @@ -37,6 +40,54 @@ class _StockItemDisplayState extends State { } void _addStock() { + showDialog(context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Add Stock"), + actions: [ + FlatButton( + child: Text("Add"), + onPressed: () { + _addStockKey.currentState.validate(); + }, + ) + ], + content: Form( + key: _addStockKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration(labelText: "Stock Quantity"), + keyboardType: TextInputType.numberWithOptions(signed:false, decimal:true), + validator: (value) { + if (value.isEmpty) { + return "Value cannot be empty"; + } + + double quantity = double.tryParse(value); + + if (quantity == null) { + return "Value cannot be converted to a number"; + } + + if (quantity <= 0) { + return "Value must be positive"; + } + + print("Adding stock!"); + + item.addStock(quantity).then((var response) { + print("added stock"); + }); + }, + ), + ], + ) + ), + ); + } + ); // TODO - Form for adding stock } From f158b0cd121ff6dad8eb9797ee8c6ae2781bfda8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 9 Apr 2020 23:21:35 +1000 Subject: [PATCH 09/17] Reload model data from server --- lib/inventree/model.dart | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index d929df88..a0e2c639 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -56,7 +56,8 @@ class InvenTreeModel { return obj; } - String get url{ return path.join(URL, pk.toString()); } + // Return the API detail endpoint for this Model object + String get url => "${URL}/${pk}/"; /* // Search this Model type in the database @@ -79,6 +80,25 @@ class InvenTreeModel { // A map of "default" headers to use when performing a GET request Map defaultGetFilters() { return Map(); } + /* + * Reload this object, by requesting data from the server + */ + void reload() async { + + print("Reloading data from $url"); + + var response = await api.get(url); + + if (response.statusCode != 200) { + print("Error retrieving data"); + return; + } + + final Map data = json.decode(response.body); + + jsondata = data; + } + // Return the detail view for the associated pk Future get(int pk, {Map filters}) async { From 80f7694abb73e4646c395db02af9d4dad7267ea1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 9 Apr 2020 23:21:53 +1000 Subject: [PATCH 10/17] Function to count stock item --- lib/inventree/stock.dart | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 9679ab6d..50ff635a 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -157,6 +157,26 @@ class InvenTreeStockItem extends InvenTreeModel { return item; } + Future countStock(double quan) async { + + // Cannot 'count' a serialized StockItem + if (isSerialized()) { + return null; + } + + // Cannot count negative stock + if (quan < 0) { + return null; + } + + return api.post("/stock/count/", body: { + "item": { + "pk": "${pk}", + "quantity": "${quan}" + } + }); + } + Future addStock(double quan) async { // Cannot add stock to a serialized StockItem @@ -169,14 +189,12 @@ class InvenTreeStockItem extends InvenTreeModel { return null; } - Map data = { + return api.post("/stock/add/", body: { "item": { "pk": "${pk}", "quantity": "${quan}", } - }; - - return api.post("/stock/add/", body: data); + }); } } From f5b8311428bb6b2d746a55a3e6638ac3caac60f4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 9 Apr 2020 23:41:16 +1000 Subject: [PATCH 11/17] Action to count stock items now works --- lib/inventree/model.dart | 8 ++- lib/widget/stock_detail.dart | 112 ++++++++++++++++++++++++++++------- 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index a0e2c639..c47f19b7 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -83,20 +83,22 @@ class InvenTreeModel { /* * Reload this object, by requesting data from the server */ - void reload() async { + Future reload() async { print("Reloading data from $url"); - var response = await api.get(url); + var response = await api.get(url, params: defaultGetFilters()); if (response.statusCode != 200) { print("Error retrieving data"); - return; + return false; } final Map data = json.decode(response.body); jsondata = data; + + return true; } // Return the detail view for the associated pk diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index cc5f782a..7e88d375 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -28,6 +28,9 @@ class StockDetailWidget extends StatefulWidget { class _StockItemDisplayState extends State { final _addStockKey = GlobalKey(); + final _takeStockKey = GlobalKey(); + final _countStockKey = GlobalKey(); + final _moveStockKey = GlobalKey(); _StockItemDisplayState(this.item) { // TODO @@ -39,7 +42,22 @@ class _StockItemDisplayState extends State { // TODO - Form for editing stock item } - void _addStock() { + void _addStock(double quantity) async { + + Navigator.of(context).pop(); + + // Await response to prevent the button from being pressed multiple times + var response = await item.addStock(quantity); + + // TODO - Handle error cases + + await item.reload(); + + setState(() {}); + + } + + void _addStockDialog() async { showDialog(context: context, builder: (BuildContext context) { return AlertDialog( @@ -55,31 +73,26 @@ class _StockItemDisplayState extends State { content: Form( key: _addStockKey, child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + Text("Current Quantity: ${item.quantity}"), TextFormField( - decoration: InputDecoration(labelText: "Stock Quantity"), + decoration: InputDecoration( + labelText: "Add stock", + ), keyboardType: TextInputType.numberWithOptions(signed:false, decimal:true), validator: (value) { - if (value.isEmpty) { - return "Value cannot be empty"; - } + if (value.isEmpty) return "Value cannot be empty"; double quantity = double.tryParse(value); + if (quantity == null) return "Value cannot be converted to a number"; + if (quantity <= 0) return "Value must be positive"; - if (quantity == null) { - return "Value cannot be converted to a number"; - } + _addStock(quantity); - if (quantity <= 0) { - return "Value must be positive"; - } - - print("Adding stock!"); - - item.addStock(quantity).then((var response) { - print("added stock"); - }); + return null; }, ), ], @@ -95,8 +108,65 @@ class _StockItemDisplayState extends State { // TODO - Form for removing stock } - void _countStock() { - // TODO - Form for counting stock + void _countStock(double quantity) async { + + Navigator.of(context).pop(); + + var response = await item.countStock(quantity); + + // TODO - Handle error cases + + await item.reload(); + + setState(() {}); + + } + + void _countStockDialog() async { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Count Stock"), + actions: [ + FlatButton( + child: Text("Count"), + onPressed: () { + _countStockKey.currentState.validate(); + }, + ) + ], + content: Form( + key: _countStockKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: "Count stock", + hintText: "${item.quantity}", + ), + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + validator: (value) { + if (value.isEmpty) return "Value cannot be empty"; + + double quantity = double.tryParse(value); + if (quantity == null) return "Value cannot be converted to a number"; + if (quantity < 0) return "Value cannot be negative"; + + _countStock(quantity); + + return null; + }, + ) + ], + ) + ) + ); + } + ); } void _transferStock() { @@ -249,7 +319,7 @@ class _StockItemDisplayState extends State { buttons.add(SpeedDialChild( child: Icon(FontAwesomeIcons.plusCircle), label: "Add Stock", - onTap: _addStock, + onTap: _addStockDialog, ) ); @@ -263,7 +333,7 @@ class _StockItemDisplayState extends State { buttons.add(SpeedDialChild( child: Icon(FontAwesomeIcons.checkCircle), label: "Count Stock", - onTap: _countStock, + onTap: _countStockDialog, )); } From b10931f3b6968812195f9726be2e2ba6be25f134 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 9 Apr 2020 23:49:39 +1000 Subject: [PATCH 12/17] Add function to remove stock --- lib/inventree/stock.dart | 22 ++++++----- lib/widget/stock_detail.dart | 76 ++++++++++++++++++++++++++++++++---- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 50ff635a..baaf4151 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -179,15 +179,7 @@ class InvenTreeStockItem extends InvenTreeModel { Future addStock(double quan) async { - // Cannot add stock to a serialized StockItem - if (isSerialized()) { - return null; - } - - // Cannot add negative stock - if (quan <= 0) { - return null; - } + if (isSerialized() || quan <= 0) return null; return api.post("/stock/add/", body: { "item": { @@ -197,6 +189,18 @@ class InvenTreeStockItem extends InvenTreeModel { }); } + Future removeStock(double quan) async { + + if (isSerialized() || quan <= 0) return null; + + return api.post("/stock/remove/", body: { + "item": { + "pk": "${pk}", + "quantity": "${quan}", + } + }); + } + } diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 7e88d375..de365939 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -28,7 +28,7 @@ class StockDetailWidget extends StatefulWidget { class _StockItemDisplayState extends State { final _addStockKey = GlobalKey(); - final _takeStockKey = GlobalKey(); + final _removeStockKey = GlobalKey(); final _countStockKey = GlobalKey(); final _moveStockKey = GlobalKey(); @@ -82,7 +82,7 @@ class _StockItemDisplayState extends State { decoration: InputDecoration( labelText: "Add stock", ), - keyboardType: TextInputType.numberWithOptions(signed:false, decimal:true), + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), validator: (value) { if (value.isEmpty) return "Value cannot be empty"; @@ -104,8 +104,65 @@ class _StockItemDisplayState extends State { // TODO - Form for adding stock } - void _removeStock() { - // TODO - Form for removing stock + void _removeStock(double quantity) async { + Navigator.of(context).pop(); + + var response = await item.removeStock(quantity); + + // TODO - Handle error cases + + await item.reload(); + + setState(() {}); + } + + void _removeStockDialog() { + showDialog(context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Remove Stock"), + actions: [ + FlatButton( + child: Text("Remove"), + onPressed: () { + _removeStockKey.currentState.validate(); + }, + ) + ], + content: Form( + key: _removeStockKey, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Current quantity: ${item.quantity}"), + TextFormField( + decoration: InputDecoration( + labelText: "Remove stock", + ), + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + validator: (value) { + if (value.isEmpty) return "Value cannot be empty"; + + double quantity = double.tryParse(value); + + if (quantity == null) return "Value cannot be converted to a number"; + if (quantity <= 0) return "Value must be positive"; + + if (quantity > item.quantity) return "Cannot take more than current quantity"; + + _removeStock(quantity); + + return null; + }, + ) + ], + ) + ), + ); + } + ); } void _countStock(double quantity) async { @@ -169,7 +226,12 @@ class _StockItemDisplayState extends State { ); } - void _transferStock() { + + void _transferStock(int location) { + // TODO + } + + void _transferStockDialog() { // TODO - Form for transferring stock } @@ -326,7 +388,7 @@ class _StockItemDisplayState extends State { buttons.add(SpeedDialChild( child: Icon(FontAwesomeIcons.minusCircle), label: "Remove Stock", - onTap: _removeStock, + onTap: _removeStockDialog, ), ); @@ -340,7 +402,7 @@ class _StockItemDisplayState extends State { buttons.add(SpeedDialChild( child: Icon(FontAwesomeIcons.exchangeAlt), label: "Transfer Stock", - onTap: _transferStock, + onTap: _transferStockDialog, )); return buttons; From 5a9dead4e6836709ffed7c76eeb4840f6566dd4b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 10 Apr 2020 00:10:39 +1000 Subject: [PATCH 13/17] Add notes field to stock actions --- lib/api.dart | 2 +- lib/inventree/model.dart | 2 -- lib/inventree/stock.dart | 17 ++++++++++------- lib/widget/stock_detail.dart | 32 ++++++++++++++++++++++++++++---- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index c2872b6b..6d272520 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -277,7 +277,7 @@ class InvenTreeAPI { var _url = makeApiUrl(url); var _headers = jsonHeaders(); - print("POST: " + _url); + print("POST: ${_url} -> ${body.toString()}"); var data = jsonEncode(body); diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index c47f19b7..a501ce7a 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -85,8 +85,6 @@ class InvenTreeModel { */ Future reload() async { - print("Reloading data from $url"); - var response = await api.get(url, params: defaultGetFilters()); if (response.statusCode != 200) { diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index baaf4151..e8cfcf22 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -157,7 +157,7 @@ class InvenTreeStockItem extends InvenTreeModel { return item; } - Future countStock(double quan) async { + Future countStock(double quan, {String notes}) async { // Cannot 'count' a serialized StockItem if (isSerialized()) { @@ -172,12 +172,13 @@ class InvenTreeStockItem extends InvenTreeModel { return api.post("/stock/count/", body: { "item": { "pk": "${pk}", - "quantity": "${quan}" - } + "quantity": "${quan}", + }, + "notes": notes ?? '', }); } - Future addStock(double quan) async { + Future addStock(double quan, {String notes}) async { if (isSerialized() || quan <= 0) return null; @@ -185,11 +186,12 @@ class InvenTreeStockItem extends InvenTreeModel { "item": { "pk": "${pk}", "quantity": "${quan}", - } + }, + "notes": notes ?? '', }); } - Future removeStock(double quan) async { + Future removeStock(double quan, {String notes}) async { if (isSerialized() || quan <= 0) return null; @@ -197,7 +199,8 @@ class InvenTreeStockItem extends InvenTreeModel { "item": { "pk": "${pk}", "quantity": "${quan}", - } + }, + "notes": notes ?? '', }); } diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index de365939..aabd6c7c 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -27,6 +27,9 @@ class StockDetailWidget extends StatefulWidget { class _StockItemDisplayState extends State { + // Single TextEditingController which can be shared between dialogs + final TextEditingController _notesController = TextEditingController(); + final _addStockKey = GlobalKey(); final _removeStockKey = GlobalKey(); final _countStockKey = GlobalKey(); @@ -47,7 +50,8 @@ class _StockItemDisplayState extends State { Navigator.of(context).pop(); // Await response to prevent the button from being pressed multiple times - var response = await item.addStock(quantity); + var response = await item.addStock(quantity, notes: _notesController.text); + _notesController.clear(); // TODO - Handle error cases @@ -95,6 +99,12 @@ class _StockItemDisplayState extends State { return null; }, ), + TextFormField( + decoration: InputDecoration( + labelText: "Notes", + ), + controller: _notesController, + ) ], ) ), @@ -107,7 +117,8 @@ class _StockItemDisplayState extends State { void _removeStock(double quantity) async { Navigator.of(context).pop(); - var response = await item.removeStock(quantity); + var response = await item.removeStock(quantity, notes: _notesController.text); + _notesController.clear(); // TODO - Handle error cases @@ -156,7 +167,13 @@ class _StockItemDisplayState extends State { return null; }, - ) + ), + TextFormField( + decoration: InputDecoration( + labelText: "Notes", + ), + controller: _notesController, + ), ], ) ), @@ -169,7 +186,8 @@ class _StockItemDisplayState extends State { Navigator.of(context).pop(); - var response = await item.countStock(quantity); + var response = await item.countStock(quantity, notes: _notesController.text); + _notesController.clear(); // TODO - Handle error cases @@ -217,6 +235,12 @@ class _StockItemDisplayState extends State { return null; }, + ), + TextFormField( + decoration: InputDecoration( + labelText: "Notes", + ), + controller: _notesController, ) ], ) From 6799e9c85b09068b195f31f077f26bdc7382216b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 10 Apr 2020 00:19:01 +1000 Subject: [PATCH 14/17] Cleanup of action dialogs --- lib/widget/stock_detail.dart | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index aabd6c7c..54cf2927 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -27,7 +27,7 @@ class StockDetailWidget extends StatefulWidget { class _StockItemDisplayState extends State { - // Single TextEditingController which can be shared between dialogs + final TextEditingController _quantityController = TextEditingController(); final TextEditingController _notesController = TextEditingController(); final _addStockKey = GlobalKey(); @@ -49,6 +49,9 @@ class _StockItemDisplayState extends State { Navigator.of(context).pop(); + double quantity = double.parse(_quantityController.text); + _quantityController.clear(); + // Await response to prevent the button from being pressed multiple times var response = await item.addStock(quantity, notes: _notesController.text); _notesController.clear(); @@ -70,7 +73,7 @@ class _StockItemDisplayState extends State { FlatButton( child: Text("Add"), onPressed: () { - _addStockKey.currentState.validate(); + if (_addStockKey.currentState.validate()) _addStock(); }, ) ], @@ -87,6 +90,7 @@ class _StockItemDisplayState extends State { labelText: "Add stock", ), keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + controller: _quantityController, validator: (value) { if (value.isEmpty) return "Value cannot be empty"; @@ -94,8 +98,6 @@ class _StockItemDisplayState extends State { if (quantity == null) return "Value cannot be converted to a number"; if (quantity <= 0) return "Value must be positive"; - _addStock(quantity); - return null; }, ), @@ -114,9 +116,12 @@ class _StockItemDisplayState extends State { // TODO - Form for adding stock } - void _removeStock(double quantity) async { + void _removeStock() async { Navigator.of(context).pop(); + double quantity = double.parse(_quantityController.text); + _quantityController.clear(); + var response = await item.removeStock(quantity, notes: _notesController.text); _notesController.clear(); @@ -136,7 +141,7 @@ class _StockItemDisplayState extends State { FlatButton( child: Text("Remove"), onPressed: () { - _removeStockKey.currentState.validate(); + if (_removeStockKey.currentState.validate()) _removeStock(); }, ) ], @@ -152,6 +157,7 @@ class _StockItemDisplayState extends State { decoration: InputDecoration( labelText: "Remove stock", ), + controller: _quantityController, keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), validator: (value) { if (value.isEmpty) return "Value cannot be empty"; @@ -163,8 +169,6 @@ class _StockItemDisplayState extends State { if (quantity > item.quantity) return "Cannot take more than current quantity"; - _removeStock(quantity); - return null; }, ), @@ -182,10 +186,13 @@ class _StockItemDisplayState extends State { ); } - void _countStock(double quantity) async { + void _countStock() async { Navigator.of(context).pop(); + double quantity = double.parse(_quantityController.text); + _quantityController.clear(); + var response = await item.countStock(quantity, notes: _notesController.text); _notesController.clear(); @@ -207,7 +214,7 @@ class _StockItemDisplayState extends State { FlatButton( child: Text("Count"), onPressed: () { - _countStockKey.currentState.validate(); + if (_countStockKey.currentState.validate()) _countStock(); }, ) ], @@ -223,6 +230,7 @@ class _StockItemDisplayState extends State { labelText: "Count stock", hintText: "${item.quantity}", ), + controller: _quantityController, keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), validator: (value) { if (value.isEmpty) return "Value cannot be empty"; @@ -231,8 +239,6 @@ class _StockItemDisplayState extends State { if (quantity == null) return "Value cannot be converted to a number"; if (quantity < 0) return "Value cannot be negative"; - _countStock(quantity); - return null; }, ), @@ -277,7 +283,7 @@ class _StockItemDisplayState extends State { ), trailing: IconButton( icon: FaIcon(FontAwesomeIcons.edit), - onPressed: _editStockItem, + onPressed: _editStockItemDialog, ) ) ) From 36a7bd1214c7cdb22dd0414a7d8484834baa2420 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 10 Apr 2020 00:21:43 +1000 Subject: [PATCH 15/17] Add framework for edit StockItem dialog --- lib/widget/stock_detail.dart | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 54cf2927..0c03c407 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -34,6 +34,7 @@ class _StockItemDisplayState extends State { final _removeStockKey = GlobalKey(); final _countStockKey = GlobalKey(); final _moveStockKey = GlobalKey(); + final _editStockKey = GlobalKey(); _StockItemDisplayState(this.item) { // TODO @@ -45,7 +46,32 @@ class _StockItemDisplayState extends State { // TODO - Form for editing stock item } - void _addStock(double quantity) async { + void _editStockItemDialog() { + + return; + // TODO - Finish implementing this + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Edit Stock Item"), + actions: [ + FlatButton( + child: Text("Save"), + onPressed: () { + if (_editStockKey.currentState.validate()) { + // TODO + } + }, + ) + ], + ); + } + ); + } + + void _addStock() async { Navigator.of(context).pop(); From 87d400415cb772a52ea1c84cd599a86a51adf9ae Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 10 Apr 2020 00:29:36 +1000 Subject: [PATCH 16/17] Add 'pull to refresh' for StockItem detail view --- lib/widget/stock_detail.dart | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 0c03c407..80a66331 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -42,6 +42,15 @@ class _StockItemDisplayState extends State { final InvenTreeStockItem item; + /** + * Function to reload the page data + */ + Future _refresh() async { + + await item.reload(); + setState(() {}); + } + void _editStockItem() { // TODO - Form for editing stock item } @@ -478,8 +487,11 @@ class _StockItemDisplayState extends State { children: actionButtons(), ), body: Center( - child: ListView( - children: stockTiles(), + child: new RefreshIndicator( + onRefresh: _refresh, + child: ListView( + children: stockTiles(), + ) ) ) ); From 63b88588ede4166935f51c180bd2cf08410aea8f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 10 Apr 2020 00:33:49 +1000 Subject: [PATCH 17/17] Toot toot - it's the tractor again! --- lib/widget/stock_detail.dart | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 80a66331..a76873e5 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -92,11 +92,7 @@ class _StockItemDisplayState extends State { _notesController.clear(); // TODO - Handle error cases - - await item.reload(); - - setState(() {}); - + _refresh(); } void _addStockDialog() async { @@ -162,9 +158,7 @@ class _StockItemDisplayState extends State { // TODO - Handle error cases - await item.reload(); - - setState(() {}); + _refresh(); } void _removeStockDialog() { @@ -233,10 +227,7 @@ class _StockItemDisplayState extends State { // TODO - Handle error cases - await item.reload(); - - setState(() {}); - + _refresh(); } void _countStockDialog() async {