diff --git a/.gitignore b/.gitignore
index 5dd3580ef6..c53a837e24 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,9 @@ local_settings.py
*.backup
*.old
+# Files used for testing
+dummy_image.*
+
# Sphinx files
docs/_build
diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py
index f0f33ad1a5..a803e6797f 100644
--- a/InvenTree/InvenTree/api_tester.py
+++ b/InvenTree/InvenTree/api_tester.py
@@ -109,12 +109,12 @@ class InvenTreeAPITestCase(APITestCase):
return response
- def patch(self, url, data, expected_code=None):
+ def patch(self, url, data, files=None, expected_code=None):
"""
Issue a PATCH request
"""
- response = self.client.patch(url, data=data, format='json')
+ response = self.client.patch(url, data=data, files=files, format='json')
if expected_code is not None:
self.assertEqual(response.status_code, expected_code)
diff --git a/InvenTree/InvenTree/ready.py b/InvenTree/InvenTree/ready.py
index 4acbcae9af..7d63861f4b 100644
--- a/InvenTree/InvenTree/ready.py
+++ b/InvenTree/InvenTree/ready.py
@@ -12,7 +12,7 @@ def isInTestMode():
return False
-def canAppAccessDatabase():
+def canAppAccessDatabase(allow_test=False):
"""
Returns True if the apps.py file can access database records.
@@ -39,6 +39,10 @@ def canAppAccessDatabase():
'compilemessages',
]
+ if not allow_test:
+ # Override for testing mode?
+ excluded_commands.append('test')
+
for cmd in excluded_commands:
if cmd in sys.argv:
return False
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index 2bc89ef637..f093255ff0 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -101,3 +101,17 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
return None
return os.path.join(str(settings.MEDIA_URL), str(value))
+
+
+class InvenTreeImageSerializerField(serializers.ImageField):
+ """
+ Custom image serializer.
+ On upload, validate that the file is a valid image file
+ """
+
+ def to_representation(self, value):
+
+ if not value:
+ return None
+
+ return os.path.join(str(settings.MEDIA_URL), str(value))
diff --git a/InvenTree/InvenTree/static/script/inventree/api.js b/InvenTree/InvenTree/static/script/inventree/api.js
index 52aba80ef5..b43bcc8419 100644
--- a/InvenTree/InvenTree/static/script/inventree/api.js
+++ b/InvenTree/InvenTree/static/script/inventree/api.js
@@ -58,7 +58,7 @@ function inventreeFormDataUpload(url, data, options={}) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
},
url: url,
- method: 'POST',
+ method: options.method || 'POST',
data: data,
processData: false,
contentType: false,
diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js
index 1a6fdec47a..79e02d7da5 100644
--- a/InvenTree/InvenTree/static/script/inventree/inventree.js
+++ b/InvenTree/InvenTree/static/script/inventree/inventree.js
@@ -219,6 +219,7 @@ function enableDragAndDrop(element, url, options) {
data - Other form data to upload
success - Callback function in case of success
error - Callback function in case of error
+ method - HTTP method
*/
data = options.data || {};
@@ -254,7 +255,8 @@ function enableDragAndDrop(element, url, options) {
if (options.error) {
options.error(xhr, status, error);
}
- }
+ },
+ method: options.method || 'POST',
}
);
} else {
diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index 93b904d55b..512c68e93b 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -61,21 +61,21 @@ def is_email_configured():
# Display warning unless in test mode
if not settings.TESTING:
- logger.warning("EMAIL_HOST is not configured")
+ logger.debug("EMAIL_HOST is not configured")
if not settings.EMAIL_HOST_USER:
configured = False
# Display warning unless in test mode
if not settings.TESTING:
- logger.warning("EMAIL_HOST_USER is not configured")
+ logger.debug("EMAIL_HOST_USER is not configured")
if not settings.EMAIL_HOST_PASSWORD:
configured = False
# Display warning unless in test mode
if not settings.TESTING:
- logger.warning("EMAIL_HOST_PASSWORD is not configured")
+ logger.debug("EMAIL_HOST_PASSWORD is not configured")
return configured
diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 4fb78cfbab..f0fe504072 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -28,7 +28,7 @@ def schedule_task(taskname, **kwargs):
try:
from django_q.models import Schedule
except (AppRegistryNotReady):
- logger.warning("Could not start background tasks - App registry not ready")
+ logger.info("Could not start background tasks - App registry not ready")
return
try:
diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py
index 8435d756fb..f196006df9 100644
--- a/InvenTree/InvenTree/test_api.py
+++ b/InvenTree/InvenTree/test_api.py
@@ -77,7 +77,7 @@ class APITests(InvenTreeAPITestCase):
self.assertIn('version', data)
self.assertIn('instance', data)
- self.assertEquals('InvenTree', data['server'])
+ self.assertEqual('InvenTree', data['server'])
def test_role_view(self):
"""
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index a47ef3b667..08fa5e0ae4 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -10,23 +10,26 @@ import common.models
INVENTREE_SW_VERSION = "0.2.4 pre"
+INVENTREE_API_VERSION = 6
+
"""
Increment thi API version number whenever there is a significant change to the API that any clients need to know about
-v3 -> 2021-05-22:
- - The updated StockItem "history tracking" now uses a different interface
+v6 -> 2021-06-23
+ - Part and Company images can now be directly uploaded via the REST API
+
+v5 -> 2021-06-21
+ - Adds API interface for manufacturer part parameters
v4 -> 2021-06-01
- BOM items can now accept "variant stock" to be assigned against them
- Many slight API tweaks were needed to get this to work properly!
-v5 -> 2021-06-21
- - Adds API interface for manufacturer part parameters
+v3 -> 2021-05-22:
+ - The updated StockItem "history tracking" now uses a different interface
"""
-INVENTREE_API_VERSION = 5
-
def inventreeInstanceName():
""" Returns the InstanceName settings for the current database """
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 06aec54c18..17caeb872d 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -337,7 +337,7 @@ class AjaxMixin(InvenTreeRoleMixin):
# Do nothing by default
pass
- def renderJsonResponse(self, request, form=None, data={}, context=None):
+ def renderJsonResponse(self, request, form=None, data=None, context=None):
""" Render a JSON response based on specific class context.
Args:
@@ -349,6 +349,9 @@ class AjaxMixin(InvenTreeRoleMixin):
Returns:
JSON response object
"""
+ # a empty dict as default can be dangerous - set it here if empty
+ if not data:
+ data = {}
if not request.is_ajax():
return HttpResponseRedirect('/')
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index 31a0031c48..b78fe1c2bf 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -205,6 +205,13 @@ class InvenTreeSetting(models.Model):
'validator': bool,
},
+ 'PART_SHOW_PRICE_IN_FORMS': {
+ 'name': _('Show Price in Forms'),
+ 'description': _('Display part price in some forms'),
+ 'default': True,
+ 'validator': bool,
+ },
+
'PART_INTERNAL_PRICE': {
'name': _('Internal Prices'),
'description': _('Enable internal prices for parts'),
diff --git a/InvenTree/company/apps.py b/InvenTree/company/apps.py
index 4366f63434..76798c5ad4 100644
--- a/InvenTree/company/apps.py
+++ b/InvenTree/company/apps.py
@@ -44,8 +44,6 @@ class CompanyConfig(AppConfig):
company.image.render_variations(replace=False)
except FileNotFoundError:
logger.warning(f"Image file '{company.image}' missing")
- company.image = None
- company.save()
except UnidentifiedImageError:
logger.warning(f"Image file '{company.image}' is invalid")
except (OperationalError, ProgrammingError):
diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py
index 471d26bd3f..1e97756987 100644
--- a/InvenTree/company/serializers.py
+++ b/InvenTree/company/serializers.py
@@ -6,14 +6,15 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount
+from InvenTree.serializers import InvenTreeModelSerializer
+from InvenTree.serializers import InvenTreeImageSerializerField
+
+from part.serializers import PartBriefSerializer
+
from .models import Company
from .models import ManufacturerPart, ManufacturerPartParameter
from .models import SupplierPart, SupplierPriceBreak
-from InvenTree.serializers import InvenTreeModelSerializer
-
-from part.serializers import PartBriefSerializer
-
class CompanyBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Company object (limited detail) """
@@ -52,7 +53,7 @@ class CompanySerializer(InvenTreeModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True)
- image = serializers.CharField(source='get_thumbnail_url', read_only=True)
+ image = InvenTreeImageSerializerField(required=False, allow_null=True)
parts_supplied = serializers.IntegerField(read_only=True)
parts_manufactured = serializers.IntegerField(read_only=True)
diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html
index 1eefade272..a276f5df4f 100644
--- a/InvenTree/company/templates/company/company_base.html
+++ b/InvenTree/company/templates/company/company_base.html
@@ -139,11 +139,17 @@
enableDragAndDrop(
"#company-thumb",
- "{% url 'company-image' company.id %}",
+ "{% url 'api-company-detail' company.id %}",
{
label: 'image',
+ method: 'PATCH',
success: function(data, status, xhr) {
- location.reload();
+
+ if (data.image) {
+ $('#company-image').attr('src', data.image);
+ } else {
+ location.reload();
+ }
}
}
);
diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py
index d5cb573f47..dd42b97801 100644
--- a/InvenTree/company/test_api.py
+++ b/InvenTree/company/test_api.py
@@ -50,10 +50,15 @@ class CompanyTest(InvenTreeAPITestCase):
self.assertEqual(response.data['name'], 'ACME')
# Change the name of the company
+ # Note we should not have the correct permissions (yet)
data = response.data
data['name'] = 'ACMOO'
- response = self.client.patch(url, data, format='json')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
+ response = self.client.patch(url, data, format='json', expected_code=400)
+
+ self.assignRole('company.change')
+
+ response = self.client.patch(url, data, format='json', expected_code=200)
+
self.assertEqual(response.data['name'], 'ACMOO')
def test_company_search(self):
diff --git a/InvenTree/order/templates/order/order_wizard/select_parts.html b/InvenTree/order/templates/order/order_wizard/select_parts.html
index 28b4a36213..a02113fa18 100644
--- a/InvenTree/order/templates/order/order_wizard/select_parts.html
+++ b/InvenTree/order/templates/order/order_wizard/select_parts.html
@@ -4,6 +4,8 @@
{% load i18n %}
{% block form %}
+{% default_currency as currency %}
+{% settings_value 'PART_SHOW_PRICE_IN_FORMS' as show_price %}
{% trans "Step 1 of 2 - Select Part Suppliers" %}
@@ -49,7 +51,13 @@
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
index c8ec42d3e7..4a8e576a6d 100644
--- a/InvenTree/order/views.py
+++ b/InvenTree/order/views.py
@@ -1004,6 +1004,15 @@ class OrderParts(AjaxView):
return ctx
+ def get_data(self):
+ """ enrich respone json data """
+ data = super().get_data()
+ # if in selection-phase, add a button to update the prices
+ if getattr(self, 'form_step', 'select_parts') == 'select_parts':
+ data['buttons'] = [{'name': 'update_price', 'title': _('Update prices')}] # set buttons
+ data['hideErrorMessage'] = '1' # hide the error message
+ return data
+
def get_suppliers(self):
""" Calculates a list of suppliers which the user will need to create POs for.
This is calculated AFTER the user finishes selecting the parts to order.
@@ -1238,9 +1247,10 @@ class OrderParts(AjaxView):
valid = False
if form_step == 'select_parts':
- # No errors? Proceed to PO selection form
- if part_errors is False:
+ # No errors? and the price-update button was not used to submit? Proceed to PO selection form
+ if part_errors is False and 'act-btn_update_price' not in request.POST:
self.ajax_template_name = 'order/order_wizard/select_pos.html'
+ self.form_step = 'select_purchase_orders' # set step (important for get_data)
else:
self.ajax_template_name = 'order/order_wizard/select_parts.html'
diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py
index 1b233bccac..0c57e2c1ab 100644
--- a/InvenTree/part/apps.py
+++ b/InvenTree/part/apps.py
@@ -39,7 +39,8 @@ class PartConfig(AppConfig):
logger.debug("InvenTree: Checking Part image thumbnails")
try:
- for part in Part.objects.all():
+ # Only check parts which have images
+ for part in Part.objects.exclude(image=None):
if part.image:
url = part.image.thumbnail.name
loc = os.path.join(settings.MEDIA_ROOT, url)
@@ -50,8 +51,7 @@ class PartConfig(AppConfig):
part.image.render_variations(replace=False)
except FileNotFoundError:
logger.warning(f"Image file '{part.image}' missing")
- part.image = None
- part.save()
+ pass
except UnidentifiedImageError:
logger.warning(f"Image file '{part.image}' is invalid")
except (OperationalError, ProgrammingError):
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 8ddf049216..2b97fde596 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -1479,16 +1479,17 @@ class Part(MPTTModel):
return True
- def get_price_info(self, quantity=1, buy=True, bom=True):
+ def get_price_info(self, quantity=1, buy=True, bom=True, internal=False):
""" Return a simplified pricing string for this part
Args:
quantity: Number of units to calculate price for
buy: Include supplier pricing (default = True)
bom: Include BOM pricing (default = True)
+ internal: Include internal pricing (default = False)
"""
- price_range = self.get_price_range(quantity, buy, bom)
+ price_range = self.get_price_range(quantity, buy, bom, internal)
if price_range is None:
return None
@@ -1576,9 +1577,10 @@ class Part(MPTTModel):
- Supplier price (if purchased from suppliers)
- BOM price (if built from other parts)
+ - Internal price (if set for the part)
Returns:
- Minimum of the supplier price or BOM price. If no pricing available, returns None
+ Minimum of the supplier, BOM or internal price. If no pricing available, returns None
"""
# only get internal price if set and should be used
@@ -2499,7 +2501,9 @@ class BomItem(models.Model):
def price_range(self):
""" Return the price-range for this BOM item. """
- prange = self.sub_part.get_price_range(self.quantity)
+ # get internal price setting
+ use_internal = common.models.InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
+ prange = self.sub_part.get_price_range(self.quantity, intenal=use_internal)
if prange is None:
return prange
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 76275bd5e1..6c47f1310f 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -7,12 +7,15 @@ from decimal import Decimal
from django.db import models
from django.db.models import Q
from django.db.models.functions import Coalesce
-from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
- InvenTreeModelSerializer)
-from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
+
from rest_framework import serializers
from sql_util.utils import SubqueryCount, SubquerySum
from djmoney.contrib.django_rest_framework import MoneyField
+
+from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
+ InvenTreeImageSerializerField,
+ InvenTreeModelSerializer)
+from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
from stock.models import StockItem
from .models import (BomItem, Part, PartAttachment, PartCategory,
@@ -300,7 +303,7 @@ class PartSerializer(InvenTreeModelSerializer):
stock_item_count = serializers.IntegerField(read_only=True)
suppliers = serializers.IntegerField(read_only=True)
- image = serializers.CharField(source='get_image_url', read_only=True)
+ image = InvenTreeImageSerializerField(required=False, allow_null=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
starred = serializers.SerializerMethodField()
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index 7e1d33bdea..b486e2c589 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -239,11 +239,19 @@
enableDragAndDrop(
'#part-thumb',
- "{% url 'part-image-upload' part.id %}",
+ "{% url 'api-part-detail' part.id %}",
{
label: 'image',
+ method: 'PATCH',
success: function(data, status, xhr) {
- location.reload();
+
+ // If image / thumbnail data present, live update
+ if (data.image) {
+ $('#part-image').attr('src', data.image);
+ } else {
+ // Otherwise, reload the page
+ location.reload();
+ }
}
}
);
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index 176149c880..d606b12637 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -1,16 +1,19 @@
# -*- coding: utf-8 -*-
-from rest_framework import status
+import PIL
from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APIClient
+
+from InvenTree.api_tester import InvenTreeAPITestCase
+from InvenTree.status_codes import StockStatus
+
from part.models import Part, PartCategory
from stock.models import StockItem
from company.models import Company
-from InvenTree.api_tester import InvenTreeAPITestCase
-from InvenTree.status_codes import StockStatus
-
class PartAPITest(InvenTreeAPITestCase):
"""
@@ -473,6 +476,74 @@ class PartDetailTests(InvenTreeAPITestCase):
self.assertEqual(response.status_code, 200)
+ def test_image_upload(self):
+ """
+ Test that we can upload an image to the part API
+ """
+
+ self.assignRole('part.add')
+
+ # Create a new part
+ response = self.client.post(
+ reverse('api-part-list'),
+ {
+ 'name': 'imagine',
+ 'description': 'All the people',
+ 'category': 1,
+ },
+ expected_code=201
+ )
+
+ pk = response.data['pk']
+
+ url = reverse('api-part-detail', kwargs={'pk': pk})
+
+ p = Part.objects.get(pk=pk)
+
+ # Part should not have an image!
+ with self.assertRaises(ValueError):
+ print(p.image.file)
+
+ # Create a custom APIClient for file uploads
+ # Ref: https://stackoverflow.com/questions/40453947/how-to-generate-a-file-upload-test-request-with-django-rest-frameworks-apireq
+ upload_client = APIClient()
+ upload_client.force_authenticate(user=self.user)
+
+ # Try to upload a non-image file
+ with open('dummy_image.txt', 'w') as dummy_image:
+ dummy_image.write('hello world')
+
+ with open('dummy_image.txt', 'rb') as dummy_image:
+ response = upload_client.patch(
+ url,
+ {
+ 'image': dummy_image,
+ },
+ format='multipart',
+ )
+
+ self.assertEqual(response.status_code, 400)
+
+ # Now try to upload a valid image file
+ img = PIL.Image.new('RGB', (128, 128), color='red')
+ img.save('dummy_image.jpg')
+
+ with open('dummy_image.jpg', 'rb') as dummy_image:
+ response = upload_client.patch(
+ url,
+ {
+ 'image': dummy_image,
+ },
+ format='multipart',
+ )
+
+ self.assertEqual(response.status_code, 200)
+
+ # And now check that the image has been set
+ p = Part.objects.get(pk=pk)
+
+ print("Image:", p.image.file)
+
class PartAPIAggregationTest(InvenTreeAPITestCase):
"""
diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py
index 4e6556e63d..85eea7ee57 100644
--- a/InvenTree/part/test_param.py
+++ b/InvenTree/part/test_param.py
@@ -23,7 +23,7 @@ class TestParams(TestCase):
def test_str(self):
t1 = PartParameterTemplate.objects.get(pk=1)
- self.assertEquals(str(t1), 'Length (mm)')
+ self.assertEqual(str(t1), 'Length (mm)')
p1 = PartParameter.objects.get(pk=1)
self.assertEqual(str(p1), 'M2x4 LPHS : Length = 4mm')
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 39e0f13b45..743b3a36b9 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -847,11 +847,13 @@ class PartPricingView(PartDetail):
# BOM Information for Pie-Chart
if part.has_bom:
+ # get internal price setting
+ use_internal = InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
ctx_bom_parts = []
# iterate over all bom-items
for item in part.bom_items.all():
ctx_item = {'name': str(item.sub_part)}
- price, qty = item.sub_part.get_price_range(quantity), item.quantity
+ price, qty = item.sub_part.get_price_range(quantity, internal=use_internal), item.quantity
price_min, price_max = 0, 0
if price: # check if price available
diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html
index 08b53be022..1bf5f8794a 100644
--- a/InvenTree/templates/InvenTree/settings/part.html
+++ b/InvenTree/templates/InvenTree/settings/part.html
@@ -20,6 +20,7 @@
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %}
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_EDIT_IPN" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" %}
+ {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
{% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" %}
{% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %}
diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js
index 06bbb7c20e..b557f0c327 100644
--- a/InvenTree/templates/js/stock.js
+++ b/InvenTree/templates/js/stock.js
@@ -179,27 +179,32 @@ function loadStockTestResultsTable(table, options) {
var match = false;
var override = false;
+ // Extract the simplified test key
var key = item.key;
// Attempt to associate this result with an existing test
- tableData.forEach(function(row, index) {
+ for (var idx = 0; idx < tableData.length; idx++) {
+
+ var row = tableData[idx];
if (key == row.key) {
item.test_name = row.test_name;
item.required = row.required;
- match = true;
-
if (row.result == null) {
item.parent = parent_node;
- tableData[index] = item;
+ tableData[idx] = item;
override = true;
} else {
item.parent = row.pk;
}
+
+ match = true;
+
+ break;
}
- });
+ }
// No match could be found
if (!match) {
diff --git a/InvenTree/users/apps.py b/InvenTree/users/apps.py
index a9f671895d..f6666e3a94 100644
--- a/InvenTree/users/apps.py
+++ b/InvenTree/users/apps.py
@@ -13,7 +13,7 @@ class UsersConfig(AppConfig):
def ready(self):
- if canAppAccessDatabase():
+ if canAppAccessDatabase(allow_test=True):
try:
self.assign_permissions()
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index 09d70c3501..2763ba0e10 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -276,7 +276,7 @@ def update_group_roles(group, debug=False):
"""
- if not canAppAccessDatabase():
+ if not canAppAccessDatabase(allow_test=True):
return
# List of permissions already associated with this group