mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 05:15:42 +00:00 
			
		
		
		
	Stock history fix (#320)
* Improves quantity parsing from * Add paginated history widget * Refactor stock history widget as a paginated widget * Allow paginated result list to handle results returned as list - Some API endpoints (older ones most likely) don't paginate results correctly * Fix code layout * Render user information in "history" widget (not quantity) * Hide filter button * Update release notes * remove unused import
This commit is contained in:
		| @@ -1,3 +1,7 @@ | |||||||
|  | org.gradle.daemon=true | ||||||
|  | org.gradle.parallel=true | ||||||
|  | org.gradle.configureondemand=true | ||||||
|  | org.gradle.caching=true | ||||||
| org.gradle.jvmargs=-Xmx1536M | org.gradle.jvmargs=-Xmx1536M | ||||||
| android.enableD8=true | android.enableD8=true | ||||||
| android.enableJetifier=true | android.enableJetifier=true | ||||||
|   | |||||||
| @@ -1,6 +1,12 @@ | |||||||
| ## InvenTree App Release Notes | ## InvenTree App Release Notes | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | ### 0.11.4 - April 2023 | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | - Bug fix for stock history widget | ||||||
|  | - Improved display of stock history widget | ||||||
|  |  | ||||||
| ### 0.11.3 - April 2023 | ### 0.11.3 - April 2023 | ||||||
| --- | --- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -575,10 +575,12 @@ class InvenTreeModel { | |||||||
|     // Construct the response |     // Construct the response | ||||||
|     InvenTreePageResponse page = InvenTreePageResponse(); |     InvenTreePageResponse page = InvenTreePageResponse(); | ||||||
|  |  | ||||||
|     var data = response.asMap(); |     var dataMap = response.asMap(); | ||||||
|  |  | ||||||
|     if (data.containsKey("count") && data.containsKey("results")) { |     // First attempt is to look for paginated data, returned as a map | ||||||
|        page.count = (data["count"] ?? 0) as int; |  | ||||||
|  |     if (dataMap.isNotEmpty && dataMap.containsKey("count") && dataMap.containsKey("results")) { | ||||||
|  |       page.count = (dataMap["count"] ?? 0) as int; | ||||||
|  |  | ||||||
|        page.results = []; |        page.results = []; | ||||||
|  |  | ||||||
| @@ -587,15 +589,28 @@ class InvenTreeModel { | |||||||
|        } |        } | ||||||
|  |  | ||||||
|        return page; |        return page; | ||||||
|  |  | ||||||
|     } else { |  | ||||||
|       return null; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Second attempt is to look for a list of data (not paginated) | ||||||
|  |     var dataList = response.asList(); | ||||||
|  |  | ||||||
|  |     if (dataList.isNotEmpty) { | ||||||
|  |       page.count = dataList.length; | ||||||
|  |       page.results = []; | ||||||
|  |  | ||||||
|  |       for (var result in dataList) { | ||||||
|  |         page.addResult(createFromJson(result as Map<String, dynamic>)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |       return page; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Finally, no results available | ||||||
|  |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Return list of objects from the database, with optional filters |   // Return list of objects from the database, with optional filters | ||||||
|   Future<List<InvenTreeModel>> list({Map<String, String> filters = const {}}) async { |   Future<List<InvenTreeModel>> list({Map<String, String> filters = const {}}) async { | ||||||
|  |  | ||||||
|     var params = defaultListFilters(); |     var params = defaultListFilters(); | ||||||
|  |  | ||||||
|     for (String key in filters.keys) { |     for (String key in filters.keys) { | ||||||
|   | |||||||
| @@ -23,9 +23,7 @@ class InvenTreeStockItemTestResult extends InvenTreeModel { | |||||||
|   @override |   @override | ||||||
|   Map<String, dynamic> formFields() { |   Map<String, dynamic> formFields() { | ||||||
|     return { |     return { | ||||||
|       "stock_item": { |       "stock_item": {"hidden": true}, | ||||||
|         "hidden": true |  | ||||||
|       }, |  | ||||||
|       "test": {}, |       "test": {}, | ||||||
|       "result": {}, |       "result": {}, | ||||||
|       "value": {}, |       "value": {}, | ||||||
| @@ -75,6 +73,7 @@ class InvenTreeStockItemHistory extends InvenTreeModel { | |||||||
|     // By default, order by decreasing date |     // By default, order by decreasing date | ||||||
|     return { |     return { | ||||||
|       "ordering": "-date", |       "ordering": "-date", | ||||||
|  |       "user_detail": "true", | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -98,21 +97,31 @@ class InvenTreeStockItemHistory extends InvenTreeModel { | |||||||
|  |  | ||||||
|   String get label => (jsondata["label"] ?? "") as String; |   String get label => (jsondata["label"] ?? "") as String; | ||||||
|  |  | ||||||
|   String get quantityString { |   // Return the "deltas" associated with this historical object | ||||||
|     Map<String, dynamic> deltas = (jsondata["deltas"] ?? {}) as Map<String, dynamic>; |   Map<String, dynamic> get deltas { | ||||||
|  |     if (jsondata.containsKey("deltas")) { | ||||||
|  |       return jsondata["deltas"] as Map<String, dynamic>; | ||||||
|  |     } else { | ||||||
|  |       return {}; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|     // Serial number takes priority here |   // Return the quantity string for this historical object | ||||||
|     if (deltas.containsKey("serial")) { |   String get quantityString { | ||||||
|       var serial = (deltas["serial"] ?? "").toString(); |     var _deltas = deltas; | ||||||
|       return "# ${serial}"; |  | ||||||
|     } else if (deltas.containsKey("quantity")) { |     if (_deltas.containsKey("quantity")) { | ||||||
|       double q = (deltas["quantity"] ?? 0) as double; |       double q = double.tryParse(_deltas["quantity"].toString()) ?? 0; | ||||||
|  |  | ||||||
|       return simpleNumberString(q); |       return simpleNumberString(q); | ||||||
|     } else { |     } else { | ||||||
|       return ""; |       return ""; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   String get userString { | ||||||
|  |     return (jsondata["user_detail"]?["username"] ?? "") as String; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,13 +1,14 @@ | |||||||
| import "package:flutter/material.dart"; | import "package:flutter/material.dart"; | ||||||
|  |  | ||||||
| import "package:inventree/widget/refreshable_state.dart"; | import "package:inventree/api.dart"; | ||||||
| import "package:inventree/l10.dart"; | import "package:inventree/l10.dart"; | ||||||
| import "package:inventree/inventree/stock.dart"; | import "package:inventree/inventree/stock.dart"; | ||||||
| import "package:inventree/inventree/model.dart"; | import "package:inventree/inventree/model.dart"; | ||||||
|  |  | ||||||
|  | import "package:inventree/widget/paginator.dart"; | ||||||
|  | import "package:inventree/widget/refreshable_state.dart"; | ||||||
|  |  | ||||||
| class StockItemHistoryWidget extends StatefulWidget { | class StockItemHistoryWidget extends StatefulWidget { | ||||||
|  |  | ||||||
|   const StockItemHistoryWidget(this.item, {Key? key}) : super(key: key); |   const StockItemHistoryWidget(this.item, {Key? key}) : super(key: key); | ||||||
|  |  | ||||||
|   final InvenTreeStockItem item; |   final InvenTreeStockItem item; | ||||||
| @@ -16,60 +17,78 @@ class StockItemHistoryWidget extends StatefulWidget { | |||||||
|   _StockItemHistoryDisplayState createState() => _StockItemHistoryDisplayState(item); |   _StockItemHistoryDisplayState createState() => _StockItemHistoryDisplayState(item); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| class _StockItemHistoryDisplayState extends RefreshableState<StockItemHistoryWidget> { | class _StockItemHistoryDisplayState extends RefreshableState<StockItemHistoryWidget> { | ||||||
|  |  | ||||||
|   _StockItemHistoryDisplayState(this.item); |   _StockItemHistoryDisplayState(this.item); | ||||||
|  |  | ||||||
|   final InvenTreeStockItem item; |   final InvenTreeStockItem item; | ||||||
|  |  | ||||||
|  |   bool showFilterOptions = false; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String getAppBarTitle() => L10().stockItemHistory; |   String getAppBarTitle() => L10().stockItemHistory; | ||||||
|  |  | ||||||
|   List<InvenTreeStockItemHistory> history = []; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<void> request(BuildContext refresh) async { |   List<Widget> appBarActions(BuildContext context) => []; | ||||||
|  |  | ||||||
|     history.clear(); |  | ||||||
|  |  | ||||||
|     await InvenTreeStockItemHistory().list(filters: {"item": "${item.pk}"}).then((List<InvenTreeModel> results) { |  | ||||||
|       for (var result in results) { |  | ||||||
|         if (result is InvenTreeStockItemHistory) { |  | ||||||
|           history.add(result); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Refresh |  | ||||||
|       setState(() { |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget getBody(BuildContext context) { |   Widget getBody(BuildContext context) { | ||||||
|     return ListView( |     Map<String, String> filters = { | ||||||
|       children: ListTile.divideTiles( |       "item": widget.item.pk.toString(), | ||||||
|         context: context, |     }; | ||||||
|         tiles: historyList(), |  | ||||||
|       ).toList() |     return PaginatedStockHistoryList(filters, showFilterOptions); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  |  * Widget which displays a paginated stock history list | ||||||
|  |  */ | ||||||
|  | class PaginatedStockHistoryList extends PaginatedSearchWidget { | ||||||
|  |   const PaginatedStockHistoryList(Map<String, String> filters, bool showSearch) | ||||||
|  |       : super(filters: filters, showSearch: showSearch); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _PaginatedStockHistoryState createState() => _PaginatedStockHistoryState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  |  * State class for the paginated stock history list | ||||||
|  |  */ | ||||||
|  | class _PaginatedStockHistoryState | ||||||
|  |     extends PaginatedSearchState<PaginatedStockHistoryList> { | ||||||
|  |   _PaginatedStockHistoryState() : super(); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String get prefix => "stock_history"; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Map<String, String> get orderingOptions => {}; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Map<String, Map<String, dynamic>> get filterOptions => { | ||||||
|  |         // TODO: Add filter options | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<InvenTreePageResponse?> requestPage( | ||||||
|  |       int limit, int offset, Map<String, String> params) async { | ||||||
|  |     await InvenTreeAPI().StockHistoryStatus.load(); | ||||||
|  |  | ||||||
|  |     final page = await InvenTreeStockItemHistory().listPaginated(limit, offset, filters: params); | ||||||
|  |  | ||||||
|  |     return page; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget buildItem(BuildContext context, InvenTreeModel model) { | ||||||
|  |     InvenTreeStockItemHistory entry = model as InvenTreeStockItemHistory; | ||||||
|  |  | ||||||
|  |     return ListTile( | ||||||
|  |       leading: Text(entry.dateString), | ||||||
|  |       trailing: entry.userString.isNotEmpty ? Text(entry.userString) : null, | ||||||
|  |       title: Text(entry.label), | ||||||
|  |       subtitle: entry.notes.isNotEmpty ? Text(entry.notes) : null, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | } | ||||||
|   List<Widget> historyList() { |  | ||||||
|     List<Widget> tiles = []; |  | ||||||
|  |  | ||||||
|     for (var entry in history) { |  | ||||||
|       tiles.add( |  | ||||||
|         ListTile( |  | ||||||
|           leading: Text(entry.dateString), |  | ||||||
|           trailing: entry.quantityString.isNotEmpty ? Text(entry.quantityString) : null, |  | ||||||
|           title: Text(entry.label), |  | ||||||
|           subtitle: entry.notes.isNotEmpty ? Text(entry.notes) : null, |  | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return tiles; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user