From faf8e45138f4848701632f6d74d45a22e4af5f85 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 26 Mar 2022 17:38:30 +1100 Subject: [PATCH 1/7] Pop to the previous context if the stock item is "bad" --- lib/widget/category_display.dart | 8 ++--- lib/widget/company_detail.dart | 4 +-- lib/widget/location_display.dart | 10 +++---- lib/widget/part_attachments_widget.dart | 4 +-- lib/widget/part_detail.dart | 10 +++---- lib/widget/part_image_widget.dart | 4 +-- lib/widget/part_notes.dart | 4 +-- lib/widget/purchase_order_detail.dart | 6 ++-- lib/widget/refreshable_state.dart | 12 ++++---- lib/widget/stock_detail.dart | 40 +++++++++++++++---------- lib/widget/stock_item_test_results.dart | 4 +-- lib/widget/stock_notes.dart | 4 +-- 12 files changed, 60 insertions(+), 50 deletions(-) diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 389d7461..c28522ff 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -66,7 +66,7 @@ class _CategoryDisplayState extends RefreshableState { context, L10().editCategory, onSuccess: (data) async { - refresh(); + refresh(context); showSnackIcon(L10().categoryUpdated, success: true); } ); @@ -79,11 +79,11 @@ class _CategoryDisplayState extends RefreshableState { @override Future onBuild(BuildContext context) async { - refresh(); + refresh(context); } @override - Future request() async { + Future request(BuildContext context) async { int pk = category?.pk ?? -1; @@ -234,7 +234,7 @@ class _CategoryDisplayState extends RefreshableState { ) ); } else { - refresh(); + refresh(context); } } ); diff --git a/lib/widget/company_detail.dart b/lib/widget/company_detail.dart index 904ced7e..b0ad3254 100644 --- a/lib/widget/company_detail.dart +++ b/lib/widget/company_detail.dart @@ -64,7 +64,7 @@ class _CompanyDetailState extends RefreshableState { } @override - Future request() async { + Future request(BuildContext context) async { await company.reload(); if (company.isSupplier) { @@ -78,7 +78,7 @@ class _CompanyDetailState extends RefreshableState { context, L10().companyEdit, onSuccess: (data) async { - refresh(); + refresh(context); showSnackIcon(L10().companyUpdated, success: true); } ); diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 2d4272c1..fb88381d 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -88,7 +88,7 @@ class _LocationDisplayState extends RefreshableState { context, L10().editLocation, onSuccess: (data) async { - refresh(); + refresh(context); showSnackIcon(L10().locationUpdated, success: true); } ); @@ -109,11 +109,11 @@ class _LocationDisplayState extends RefreshableState { @override Future onBuild(BuildContext context) async { - refresh(); + refresh(context); } @override - Future request() async { + Future request(BuildContext context) async { int pk = location?.pk ?? -1; @@ -385,8 +385,8 @@ List detailTiles() { MaterialPageRoute(builder: (context) => InvenTreeQRView( StockLocationScanInItemsHandler(_loc))) - ).then((context) { - refresh(); + ).then((value) { + refresh(context); }); } }, diff --git a/lib/widget/part_attachments_widget.dart b/lib/widget/part_attachments_widget.dart index 2e14141f..29c32e6c 100644 --- a/lib/widget/part_attachments_widget.dart +++ b/lib/widget/part_attachments_widget.dart @@ -72,11 +72,11 @@ class _PartAttachmentDisplayState extends RefreshableState request() async { + Future request(BuildContext context) async { await InvenTreePartAttachment().list( filters: { diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index a00c92ac..3357e71e 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -77,7 +77,7 @@ class _PartDisplayState extends RefreshableState { @override Future onBuild(BuildContext context) async { - refresh(); + refresh(context); setState(() { @@ -85,7 +85,7 @@ class _PartDisplayState extends RefreshableState { } @override - Future request() async { + Future request(BuildContext context) async { await part.reload(); await part.getTestTemplates(); } @@ -94,7 +94,7 @@ class _PartDisplayState extends RefreshableState { if (InvenTreeAPI().checkPermission("part", "view")) { await part.update(values: {"starred": "${!part.starred}"}); - refresh(); + refresh(context); } } @@ -104,7 +104,7 @@ class _PartDisplayState extends RefreshableState { context, L10().editPart, onSuccess: (data) async { - refresh(); + refresh(context); showSnackIcon(L10().partEdited, success: true); } ); @@ -130,7 +130,7 @@ class _PartDisplayState extends RefreshableState { builder: (context) => PartImageWidget(part) ) ).then((value) { - refresh(); + refresh(context); }); }), ), diff --git a/lib/widget/part_image_widget.dart b/lib/widget/part_image_widget.dart index 63981371..e9ed62e4 100644 --- a/lib/widget/part_image_widget.dart +++ b/lib/widget/part_image_widget.dart @@ -32,7 +32,7 @@ class _PartImageState extends RefreshableState { final InvenTreePart part; @override - Future request() async { + Future request(BuildContext context) async { await part.reload(); } @@ -60,7 +60,7 @@ class _PartImageState extends RefreshableState { showSnackIcon(L10().uploadFailed, success: false); } - refresh(); + refresh(context); } ); diff --git a/lib/widget/part_notes.dart b/lib/widget/part_notes.dart index e2618d2f..717a8fe5 100644 --- a/lib/widget/part_notes.dart +++ b/lib/widget/part_notes.dart @@ -26,7 +26,7 @@ class _PartNotesState extends RefreshableState { final InvenTreePart part; @override - Future request() async { + Future request(BuildContext context) async { await part.reload(); } @@ -53,7 +53,7 @@ class _PartNotesState extends RefreshableState { } }, onSuccess: (data) async { - refresh(); + refresh(context); } ); } diff --git a/lib/widget/purchase_order_detail.dart b/lib/widget/purchase_order_detail.dart index d6cdfea2..27a47bd3 100644 --- a/lib/widget/purchase_order_detail.dart +++ b/lib/widget/purchase_order_detail.dart @@ -61,7 +61,7 @@ class _PurchaseOrderDetailState extends RefreshableState request() async { + Future request(BuildContext context) async { await order.reload(); lines = await order.getLineItems(); @@ -82,7 +82,7 @@ class _PurchaseOrderDetailState extends RefreshableState extends State { // Function called after the widget is first build Future onBuild(BuildContext context) async { - refresh(); + refresh(context); } // Function to request data for this page - Future request() async { + Future request(BuildContext context) async { return; } - Future refresh() async { + Future refresh(BuildContext context) async { setState(() { loading = true; }); - await request(); + await request(context); setState(() { loading = false; @@ -100,7 +100,9 @@ abstract class RefreshableState extends State { body: Builder( builder: (BuildContext context) { return RefreshIndicator( - onRefresh: refresh, + onRefresh: () async { + refresh(context); + }, child: getBody(context) ); } diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 36f67dcb..69af88a7 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -22,6 +22,7 @@ import "package:inventree/l10.dart"; import "package:inventree/helpers.dart"; import "package:inventree/api.dart"; import "package:inventree/api_form.dart"; +import 'package:one_context/one_context.dart'; class StockDetailWidget extends StatefulWidget { @@ -96,13 +97,20 @@ class _StockItemDisplayState extends RefreshableState { // Load part data if not already loaded if (part == null) { - refresh(); + refresh(context); } } @override - Future request() async { - await item.reload(); + Future request(BuildContext context) async { + + final bool result = await item.reload(); + + // Could not load this stock item for some reason + // Perhaps it has been depleted? + if (!result || item.pk == -1) { + Navigator.of(context).pop(); + } // Request part information part = await InvenTreePart().get(item.partId) as InvenTreePart?; @@ -244,7 +252,7 @@ class _StockItemDisplayState extends RefreshableState { L10().editItem, fields: fields, onSuccess: (data) async { - refresh(); + refresh(context); showSnackIcon(L10().stockItemUpdated, success: true); } ); @@ -261,7 +269,7 @@ class _StockItemDisplayState extends RefreshableState { _stockUpdateMessage(result); - refresh(); + refresh(context); } Future _addStockDialog() async { @@ -293,7 +301,7 @@ class _StockItemDisplayState extends RefreshableState { icon: FontAwesomeIcons.plusCircle, onSuccess: (data) async { _stockUpdateMessage(true); - refresh(); + refresh(context); } ); @@ -340,7 +348,7 @@ class _StockItemDisplayState extends RefreshableState { _stockUpdateMessage(result); - refresh(); + refresh(context); } @@ -372,7 +380,7 @@ class _StockItemDisplayState extends RefreshableState { icon: FontAwesomeIcons.minusCircle, onSuccess: (data) async { _stockUpdateMessage(true); - refresh(); + refresh(context); } ); @@ -413,7 +421,7 @@ class _StockItemDisplayState extends RefreshableState { _stockUpdateMessage(result); - refresh(); + refresh(context); } Future _countStockDialog() async { @@ -445,7 +453,7 @@ class _StockItemDisplayState extends RefreshableState { icon: FontAwesomeIcons.clipboardCheck, onSuccess: (data) async { _stockUpdateMessage(true); - refresh(); + refresh(context); } ); @@ -494,7 +502,7 @@ class _StockItemDisplayState extends RefreshableState { ); } - refresh(); + refresh(context); } @@ -509,7 +517,7 @@ class _StockItemDisplayState extends RefreshableState { var result = await item.transferStock(context, locationId, quantity: quantity, notes: notes); - refresh(); + refresh(context); if (result) { showSnackIcon(L10().stockItemTransferred, success: true); @@ -549,7 +557,7 @@ class _StockItemDisplayState extends RefreshableState { icon: FontAwesomeIcons.dolly, onSuccess: (data) async { _stockUpdateMessage(true); - refresh(); + refresh(context); } ); @@ -801,7 +809,7 @@ class _StockItemDisplayState extends RefreshableState { MaterialPageRoute( builder: (context) => StockItemTestResultsWidget(item)) ).then((context) { - refresh(); + refresh(context); }); } ) @@ -928,7 +936,7 @@ class _StockItemDisplayState extends RefreshableState { context, MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemScanIntoLocationHandler(item))) ).then((context) { - refresh(); + refresh(context); }); }, ) @@ -958,7 +966,7 @@ class _StockItemDisplayState extends RefreshableState { icon: Icons.qr_code, ); - refresh(); + refresh(context); } }); }); diff --git a/lib/widget/stock_item_test_results.dart b/lib/widget/stock_item_test_results.dart index d7d3a7cf..933286a2 100644 --- a/lib/widget/stock_item_test_results.dart +++ b/lib/widget/stock_item_test_results.dart @@ -44,7 +44,7 @@ class _StockItemTestResultDisplayState extends RefreshableState request() async { + Future request(BuildContext context) async { await item.getTestTemplates(); await item.getTestResults(); } @@ -61,7 +61,7 @@ class _StockItemTestResultDisplayState extends RefreshableState { String getAppBarTitle(BuildContext context) => L10().stockItemNotes; @override - Future request() async { + Future request(BuildContext context) async { await item.reload(); } @@ -54,7 +54,7 @@ class _StockNotesState extends RefreshableState { } }, onSuccess: (data) async { - refresh(); + refresh(context); } ); } From 6c3b83c05bc8e7fe654c850c33f7be91134226d6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 26 Mar 2022 17:40:02 +1100 Subject: [PATCH 2/7] Same thing, but for the "part" display --- lib/widget/part_detail.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 3357e71e..40c5ce4a 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -86,7 +86,14 @@ class _PartDisplayState extends RefreshableState { @override Future request(BuildContext context) async { - await part.reload(); + + final bool result = await part.reload(); + + if (!result || part.pk == -1) { + // Part could not be loaded, for some reason + Navigator.of(context).pop(); + } + await part.getTestTemplates(); } From ea724fcf5ffbafe7aea12dd9914152a0b03023db Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 26 Mar 2022 18:33:02 +1100 Subject: [PATCH 3/7] Allow user to manually remove (delete) a StockItem --- lib/api.dart | 34 ++++++++++++++++++++++++++++++++-- lib/inventree/model.dart | 36 +++++++++++++++++++++++++++++++++--- lib/widget/dialogs.dart | 4 ++-- lib/widget/stock_detail.dart | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 7 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 34e4a1c4..af67fac6 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -918,7 +918,7 @@ class InvenTreeAPI { /* * Complete an API request, and return an APIResponse object */ - Future completeRequest(HttpClientRequest request, {String? data, int? statusCode}) async { + Future completeRequest(HttpClientRequest request, {String? data, int? statusCode, bool ignoreResponse = false}) async { if (data != null && data.isNotEmpty) { @@ -955,7 +955,12 @@ class InvenTreeAPI { ); } else { - response.data = await responseToJson(_response) ?? {}; + + if (ignoreResponse) { + response.data = {}; + } else { + response.data = await responseToJson(_response) ?? {}; + } if (statusCode != null) { @@ -1042,6 +1047,31 @@ class InvenTreeAPI { return completeRequest(request); } + /* + * Perform a HTTP DELETE request + */ + Future delete(String url) async { + + HttpClientRequest? request = await apiRequest( + url, + "DELETE", + ); + + if (request == null) { + // Return an "invalid" APIResponse object + return APIResponse( + url: url, + method: "DELETE", + error: "HttpClientRequest is null", + ); + } + + return completeRequest( + request, + ignoreResponse: true, + ); + } + // Return a list of request headers Map defaultHeaders() { Map headers = {}; diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index a9eba502..116b2e08 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -222,6 +222,38 @@ class InvenTreeModel { return {}; } + /// Delete the instance on the remote server + /// Returns true if the operation was successful, else false + Future delete() async { + var response = await api.delete(url); + + if (!response.isValid() || response.data == null || (response.data is! Map)) { + + if (response.statusCode > 0) { + await sentryReportMessage( + "InvenTreeModel.delete() returned invalid response", + context: { + "url": url, + "statusCode": response.statusCode.toString(), + "data": response.data?.toString() ?? "null", + "error": response.error, + "errorDetail": response.errorDetail, + } + ); + } + + showServerError( + L10().serverError, + L10().errorDelete, + ); + + return false; + } + + // Status code should be 204 for "record deleted" + return response.statusCode == 204; + } + /* * Reload this object, by requesting data from the server */ @@ -242,7 +274,7 @@ class InvenTreeModel { "valid": response.isValid().toString(), "error": response.error, "errorDetail": response.errorDetail, - } + }, ); } @@ -466,8 +498,6 @@ class InvenTreeModel { // Provide a listing of objects at the endpoint // TODO - Static function which returns a list of objects (of this class) - // TODO - Define a "delete" function - // TODO - Define a "save" / "update" function // Override this function for each sub-class diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 6dca2ec5..e5a1e8d9 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -8,7 +8,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/l10.dart"; import "package:one_context/one_context.dart"; -Future confirmationDialog(String title, String text, {String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async { +Future confirmationDialog(String title, String text, {IconData icon = FontAwesomeIcons.questionCircle, String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async { String _accept = acceptText ?? L10().ok; String _reject = rejectText ?? L10().cancel; @@ -18,7 +18,7 @@ Future confirmationDialog(String title, String text, {String? acceptText, return AlertDialog( title: ListTile( title: Text(title), - leading: FaIcon(FontAwesomeIcons.questionCircle), + leading: FaIcon(icon), ), content: Text(text), actions: [ diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 69af88a7..43242f7b 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -157,6 +157,27 @@ class _StockItemDisplayState extends RefreshableState { }); } + /// Delete the stock item from the database + Future _deleteItem(BuildContext context) async { + + confirmationDialog( + L10().stockItemDelete, + L10().stockItemDeleteConfirm, + icon: FontAwesomeIcons.trashAlt, + onAccept: () async { + final bool result = await item.delete(); + + if (result) { + Navigator.of(context).pop(); + showSnackIcon(L10().stockItemDeleteSuccess, success: true); + } else { + showSnackIcon(L10().stockItemDeleteFailure, success: false); + } + }, + ); + + } + /// Opens a popup dialog allowing user to select a label for printing Future _printLabel(BuildContext context) async { @@ -1003,6 +1024,19 @@ class _StockItemDisplayState extends RefreshableState { ); } + // If the user has permission to delete this stock item + if (InvenTreeAPI().checkPermission("stock", "delete")) { + tiles.add( + ListTile( + title: Text("Delete Stock Item"), + leading: FaIcon(FontAwesomeIcons.trashAlt, color: COLOR_DANGER), + onTap: () { + _deleteItem(context); + }, + ) + ); + } + return tiles; } From 6d3ca0fa3daae3cdb34faeb16ba656d139a3a331 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 26 Mar 2022 18:33:17 +1100 Subject: [PATCH 4/7] Translations --- lib/l10n | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n b/lib/l10n index 42662619..a97cbdd8 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit 42662619b3d094e9bd41d3f715cac2beff7cf6f6 +Subproject commit a97cbdd84d2114227add24970cfd5a4d3a5c5f2c From 8ba4a9e678bfe6158661c5497a49a5ade7d90982 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 26 Mar 2022 18:38:10 +1100 Subject: [PATCH 5/7] Similar checks for other widgets --- lib/widget/category_display.dart | 6 +++++- lib/widget/location_display.dart | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index c28522ff..d0c883e2 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -89,7 +89,11 @@ class _CategoryDisplayState extends RefreshableState { // Update the category if (category != null) { - await category!.reload(); + final bool result = await category?.reload() ?? false; + + if (!result) { + Navigator.of(context).pop(); + } } // Request a list of sub-categories under this one diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index fb88381d..0c2fd3a3 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -119,7 +119,11 @@ class _LocationDisplayState extends RefreshableState { // Reload location information if (location != null) { - await location?.reload(); + final bool result = await location?.reload() ?? false; + + if (!result) { + Navigator.of(context).pop(); + } } // Request a list of sub-locations under this one From 6c7b4ea7c16947558b9cb79cb7b3bac7494fdd87 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 26 Mar 2022 19:21:24 +1100 Subject: [PATCH 6/7] Update release notes --- analysis_options.yaml | 4 ++++ assets/release_notes.md | 1 + lib/widget/stock_detail.dart | 5 ++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 418cc57b..8727159e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -63,3 +63,7 @@ linter: always_specify_types: false avoid_unnecessary_containers: false + + require_trailing_commas: false + + eol_at_end_of_file: false \ No newline at end of file diff --git a/assets/release_notes.md b/assets/release_notes.md index 405491a9..04335fc8 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -5,6 +5,7 @@ --- - Enables printing of stock item labels +- Allow users to manually delete stock items ### 0.5.6 - January 2022 --- diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index d7611787..4733674f 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -22,7 +22,6 @@ import "package:inventree/l10.dart"; import "package:inventree/helpers.dart"; import "package:inventree/api.dart"; import "package:inventree/api_form.dart"; -import 'package:one_context/one_context.dart'; class StockDetailWidget extends StatefulWidget { @@ -842,7 +841,7 @@ class _StockItemDisplayState extends RefreshableState { context, MaterialPageRoute( builder: (context) => StockItemTestResultsWidget(item)) - ).then((context) { + ).then((ctx) { refresh(context); }); } @@ -969,7 +968,7 @@ class _StockItemDisplayState extends RefreshableState { Navigator.push( context, MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemScanIntoLocationHandler(item))) - ).then((context) { + ).then((ctx) { refresh(context); }); }, From 519c8436ef6782b2fccf8bb1456107bddbe3f4ae Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 26 Mar 2022 19:34:22 +1100 Subject: [PATCH 7/7] Fixes --- lib/widget/part_suppliers.dart | 2 +- lib/widget/starred_parts.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widget/part_suppliers.dart b/lib/widget/part_suppliers.dart index fc1b4030..a69d5f22 100644 --- a/lib/widget/part_suppliers.dart +++ b/lib/widget/part_suppliers.dart @@ -32,7 +32,7 @@ class _PartSupplierState extends RefreshableState { List _supplierParts = []; @override - Future request() async { + Future request(BuildContext context) async { // TODO - Request list of suppliers for the part await part.reload(); _supplierParts = await part.getSupplierParts(); diff --git a/lib/widget/starred_parts.dart b/lib/widget/starred_parts.dart index fbb33936..a96977c5 100644 --- a/lib/widget/starred_parts.dart +++ b/lib/widget/starred_parts.dart @@ -27,7 +27,7 @@ class _StarredPartState extends RefreshableState { String getAppBarTitle(BuildContext context) => L10().partsStarred; @override - Future request() async { + Future request(BuildContext context) async { final parts = await InvenTreePart().list(filters: {"starred": "true"});