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/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/category_display.dart b/lib/widget/category_display.dart index 389d7461..d0c883e2 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,17 +79,21 @@ 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; // 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 @@ -234,7 +238,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/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/location_display.dart b/lib/widget/location_display.dart index 2d4272c1..0c2fd3a3 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,17 +109,21 @@ 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; // 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 @@ -385,8 +389,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..40c5ce4a 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,8 +85,15 @@ class _PartDisplayState extends RefreshableState { } @override - Future request() async { - await part.reload(); + Future request(BuildContext context) async { + + 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(); } @@ -94,7 +101,7 @@ class _PartDisplayState extends RefreshableState { if (InvenTreeAPI().checkPermission("part", "view")) { await part.update(values: {"starred": "${!part.starred}"}); - refresh(); + refresh(context); } } @@ -104,7 +111,7 @@ class _PartDisplayState extends RefreshableState { context, L10().editPart, onSuccess: (data) async { - refresh(); + refresh(context); showSnackIcon(L10().partEdited, success: true); } ); @@ -130,7 +137,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/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/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/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"}); diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 9a1ac7ae..4733674f 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -96,13 +96,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?; @@ -149,6 +156,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 { @@ -244,7 +272,7 @@ class _StockItemDisplayState extends RefreshableState { L10().editItem, fields: fields, onSuccess: (data) async { - refresh(); + refresh(context); showSnackIcon(L10().stockItemUpdated, success: true); } ); @@ -261,7 +289,7 @@ class _StockItemDisplayState extends RefreshableState { _stockUpdateMessage(result); - refresh(); + refresh(context); } Future _addStockDialog() async { @@ -293,7 +321,7 @@ class _StockItemDisplayState extends RefreshableState { icon: FontAwesomeIcons.plusCircle, onSuccess: (data) async { _stockUpdateMessage(true); - refresh(); + refresh(context); } ); @@ -340,7 +368,7 @@ class _StockItemDisplayState extends RefreshableState { _stockUpdateMessage(result); - refresh(); + refresh(context); } @@ -372,7 +400,7 @@ class _StockItemDisplayState extends RefreshableState { icon: FontAwesomeIcons.minusCircle, onSuccess: (data) async { _stockUpdateMessage(true); - refresh(); + refresh(context); } ); @@ -413,7 +441,7 @@ class _StockItemDisplayState extends RefreshableState { _stockUpdateMessage(result); - refresh(); + refresh(context); } Future _countStockDialog() async { @@ -445,7 +473,7 @@ class _StockItemDisplayState extends RefreshableState { icon: FontAwesomeIcons.clipboardCheck, onSuccess: (data) async { _stockUpdateMessage(true); - refresh(); + refresh(context); } ); @@ -494,7 +522,7 @@ class _StockItemDisplayState extends RefreshableState { ); } - refresh(); + refresh(context); } @@ -509,7 +537,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 +577,7 @@ class _StockItemDisplayState extends RefreshableState { icon: FontAwesomeIcons.dolly, onSuccess: (data) async { _stockUpdateMessage(true); - refresh(); + refresh(context); } ); @@ -813,8 +841,8 @@ class _StockItemDisplayState extends RefreshableState { context, MaterialPageRoute( builder: (context) => StockItemTestResultsWidget(item)) - ).then((context) { - refresh(); + ).then((ctx) { + refresh(context); }); } ) @@ -940,8 +968,8 @@ class _StockItemDisplayState extends RefreshableState { Navigator.push( context, MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemScanIntoLocationHandler(item))) - ).then((context) { - refresh(); + ).then((ctx) { + refresh(context); }); }, ) @@ -971,7 +999,7 @@ class _StockItemDisplayState extends RefreshableState { icon: Icons.qr_code, ); - refresh(); + refresh(context); } }); }); @@ -1008,6 +1036,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; } 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); } ); }