From 21f209f7cce0f12c306f5e37dbd681dda05072cd Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Fri, 16 Feb 2024 08:47:05 +1100
Subject: [PATCH] Forms actions fix (#6493)

* Handle case where OPTIONS.actions is not present

* Specify stock.change permission

* Hide table button based on user permission

* Fix for permission check class
---
 InvenTree/InvenTree/permissions.py         | 4 ++++
 InvenTree/stock/api.py                     | 2 ++
 InvenTree/stock/templates/stock/item.html  | 1 +
 InvenTree/templates/js/translated/forms.js | 6 +++++-
 InvenTree/templates/js/translated/stock.js | 5 ++++-
 5 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/InvenTree/InvenTree/permissions.py b/InvenTree/InvenTree/permissions.py
index dee5e0256d..da472b8637 100644
--- a/InvenTree/InvenTree/permissions.py
+++ b/InvenTree/InvenTree/permissions.py
@@ -69,6 +69,10 @@ class RolePermission(permissions.BasePermission):
 
         # The required role may be defined for the view class
         if role := getattr(view, 'role_required', None):
+            # If the role is specified as "role.permission", split it
+            if '.' in role:
+                role, permission = role.split('.')
+
             return users.models.check_user_role(user, role, permission)
 
         try:
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index ac46683243..c601f6478a 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -123,6 +123,8 @@ class StockDetail(RetrieveUpdateDestroyAPI):
 class StockItemContextMixin:
     """Mixin class for adding StockItem object to serializer context."""
 
+    role_required = 'stock.change'
+
     queryset = StockItem.objects.none()
 
     def get_serializer_context(self):
diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html
index d811aa17f3..4512d13d5d 100644
--- a/InvenTree/stock/templates/stock/item.html
+++ b/InvenTree/stock/templates/stock/item.html
@@ -196,6 +196,7 @@
             stock_item: {{ item.pk }},
             part: {{ item.part.pk }},
             quantity: {{ item.quantity|unlocalize }},
+            can_edit: {% js_bool roles.stock.change %},
         }
     );
 
diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js
index e5b075942e..6d448a0d14 100644
--- a/InvenTree/templates/js/translated/forms.js
+++ b/InvenTree/templates/js/translated/forms.js
@@ -346,7 +346,11 @@ function constructForm(url, options={}) {
     getApiEndpointOptions(url, function(OPTIONS) {
 
         // Copy across entire actions struct
-        options.actions = OPTIONS.actions.POST || OPTIONS.actions.PUT || OPTIONS.actions.PATCH || OPTIONS.actions.DELETE || {};
+        if (OPTIONS && OPTIONS.actions) {
+            options.actions = OPTIONS.actions.POST || OPTIONS.actions.PUT || OPTIONS.actions.PATCH || OPTIONS.actions.DELETE || {};
+        } else {
+            options.actions = {};
+        }
 
         // Extract any custom 'context' information from the OPTIONS data
         options.context = OPTIONS.context || {};
diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js
index 88613f09bc..6790ee20a8 100644
--- a/InvenTree/templates/js/translated/stock.js
+++ b/InvenTree/templates/js/translated/stock.js
@@ -3101,11 +3101,14 @@ function loadInstalledInTable(table, options) {
                 field: 'buttons',
                 title: '',
                 switchable: false,
+                visible: options.can_edit,
                 formatter: function(value, row) {
                     let pk = row.pk;
                     let html = '';
 
-                    html += makeIconButton('fa-unlink', 'button-uninstall', pk, '{% trans "Uninstall Stock Item" %}');
+                    if (options.can_edit) {
+                        html += makeIconButton('fa-unlink', 'button-uninstall', pk, '{% trans "Uninstall Stock Item" %}');
+                    }
 
                     return wrapButtons(html);
                 }