From ea724fcf5ffbafe7aea12dd9914152a0b03023db Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 26 Mar 2022 18:33:02 +1100
Subject: [PATCH] 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<APIResponse> completeRequest(HttpClientRequest request, {String? data, int? statusCode}) async {
+  Future<APIResponse> 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<APIResponse> 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<String, String> defaultHeaders() {
     Map<String, String> 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<bool> 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<void> confirmationDialog(String title, String text, {String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async {
+Future<void> 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<void> 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<StockDetailWidget> {
     });
   }
 
+  /// Delete the stock item from the database
+  Future<void> _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 <void> _printLabel(BuildContext context) async {
 
@@ -1003,6 +1024,19 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
       );
     }
 
+    // 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;
   }