feat: Add facility to completely hide parts of the interface (#12842)

* fix typing issue for pylance engine

* add flags to mark rough function of setting

* update pr version

* fix settings serialisation

* render security warnings

* move Transfer Orders

* respect return order hiding

* almost all confirm texts will have a warning

* add settings for sales, purchase and buildorder deactivation

* make permission check also ensure gloablsetting for view is available; reduce number of chained gates

* document possible missing check

* ensure we can still show the sales tab

* add additional warning to group cleanup

* hide more ui parts conditionally

* add tests for new code and remove duplication

* fix test

* alternative approach to fix SSO path

* remove patch; now done in serializer

* ignore ldap section
This commit is contained in:
Matthias Mair
2026-09-15 21:13:59 +10:00
committed by GitHub
parent 1afe583ea5
commit 63a434c652
40 changed files with 339 additions and 157 deletions
+1
View File
@@ -285,6 +285,7 @@ The following [global settings](../settings/global.md) are available for adjusti
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("BUILDORDER_ENABLED") }}
{{ globalsetting("BUILDORDER_REFERENCE_PATTERN") }}
{{ globalsetting("BUILDORDER_REQUIRE_RESPONSIBLE") }}
{{ globalsetting("BUILDORDER_REQUIRE_ACTIVE_PART") }}
+1
View File
@@ -206,6 +206,7 @@ The following [global settings](../settings/global.md) are available for purchas
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("PURCHASEORDER_ENABLED") }}
{{ globalsetting("PURCHASEORDER_REFERENCE_PATTERN") }}
{{ globalsetting("PURCHASEORDER_REQUIRE_RESPONSIBLE") }}
{{ globalsetting("PURCHASEORDER_CONVERT_CURRENCY") }}
+1
View File
@@ -287,6 +287,7 @@ The following [global settings](../settings/global.md) are available for sales o
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("SALESORDER_ENABLED") }}
{{ globalsetting("SALESORDER_REFERENCE_PATTERN") }}
{{ globalsetting("SALESORDER_REQUIRE_RESPONSIBLE") }}
{{ globalsetting("SALESORDER_DEFAULT_SHIPMENT") }}
@@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 545
INVENTREE_API_VERSION = 546
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v546 -> 2026-09-12 : https://github.com/inventree/InvenTree/pull/12842/changes
- Added setting flags to the settings APIs (read-only)
v545 -> 2026-09-08 : https://github.com/inventree/InvenTree/pull/12808
- Ensure consistent ordering of SSO options in API documentation
@@ -149,7 +149,6 @@ class RegistrationMixin:
def save_user(self, request, user, form, commit=True):
"""Check if a default group is set in settings."""
user._is_registering = True # marker for email synchronization
# Create the user
user = super().save_user(request, user, form)
+8
View File
@@ -1013,6 +1013,14 @@ class BaseInvenTreeSetting(models.Model):
return setting.get('confirm_text', '')
def flags(self) -> list:
"""Return the flags associated with this setting."""
setting = self.get_setting_definition(
self.key, **self.get_filters_for_instance()
)
return setting.get('flags', [])
def model_filters(self) -> Optional[dict]:
"""Return the model filters associated with this setting."""
setting = self.get_setting_definition(
@@ -132,6 +132,12 @@ class SettingsSerializer(InvenTreeModelSerializer):
confirm_text = serializers.CharField(read_only=True)
flags = serializers.ListField(
child=serializers.CharField(),
read_only=True,
help_text=_('Indicating behavior or purpose of setting.'),
)
def is_valid(self, *, raise_exception=False):
"""Validate the setting, including confirmation if required."""
ret = super().is_valid(raise_exception=raise_exception)
@@ -169,6 +175,7 @@ class GlobalSettingsSerializer(SettingsSerializer):
'read_only',
'confirm',
'confirm_text',
'flags',
]
read_only = serializers.SerializerMethodField(
@@ -214,6 +221,7 @@ class UserSettingsSerializer(SettingsSerializer):
'typ',
'confirm',
'confirm_text',
'flags',
]
user = serializers.PrimaryKeyRelatedField(read_only=True)
@@ -264,6 +272,7 @@ class GenericReferencedSettingSerializer(SettingsSerializer):
'required',
'confirm',
'confirm_text',
'flags',
]
# set Meta class
+82 -16
View File
@@ -18,7 +18,7 @@ import common.currency
import common.validators
import order.validators
import report.helpers
from common.setting.type import InvenTreeSettingsKeyType
from common.setting.type import InvenTreeSettingsKeyType, SettingFlag
def validate_part_name_format(value):
@@ -60,41 +60,40 @@ def validate_part_name_format(value):
return True
def update_instance_name(setting):
"""Update the first site objects name to instance name."""
def _get_site_object():
"""Return the first site object, or None if it doesn't exist."""
if not django_settings.SITE_MULTI:
return
return # pragma: no cover
try:
from django.contrib.sites.models import Site
except (ImportError, RuntimeError):
# Multi-site support not enabled
return
return # pragma: no cover
site_obj = Site.objects.all().order_by('id').first()
if site_obj is None:
return # pragma: no cover
return site_obj
def update_instance_name(setting):
"""Update the first site objects name to instance name."""
if site_obj := _get_site_object():
site_obj.name = setting.value
site_obj.save()
def update_instance_url(setting):
"""Update the first site objects domain to url."""
if not django_settings.SITE_MULTI:
return
try:
from django.contrib.sites.models import Site
except (ImportError, RuntimeError):
# Multi-site support not enabled
return
site_obj = Site.objects.all().order_by('id').first()
if site_obj := _get_site_object():
site_obj.domain = setting.value
site_obj.save()
def settings_group_options():
"""Build up group tuple for settings based on your choices."""
return [('', _('No group')), *[(str(a.id), str(a)) for a in Group.objects.all()]]
return [('', _('No group')), *[(str(a.pk), str(a)) for a in Group.objects.all()]]
def reload_plugin_registry(setting):
@@ -189,12 +188,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
'hidden': True,
'flags': [SettingFlag.INTERNAL],
},
'_PENDING_MIGRATIONS': {
'name': _('Pending migrations'),
'description': _('Number of pending database migrations'),
'default': 0,
'validator': int,
'flags': [SettingFlag.INTERNAL],
},
SystemSetId.GLOBAL_WARNING: {
'name': _('Active warning codes'),
@@ -202,6 +203,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'validator': json.loads,
'default': '{}',
'hidden': True,
'flags': [SettingFlag.INTERNAL],
},
'INVENTREE_INSTANCE_ID': {
'name': _('Instance ID'),
@@ -240,6 +242,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Show a warning banner in the UI when logged in as superuser'),
'validator': bool,
'default': True,
'flags': [SettingFlag.SECURITY],
},
'INVENTREE_SHOW_ADMIN_BANNER': {
'name': _('Show admin banner'),
@@ -292,12 +295,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'units': 'MB',
'default': 10,
'validator': [int, MinValueValidator(1)],
'flags': [SettingFlag.SECURITY],
},
'INVENTREE_STRICT_URLS': {
'name': _('Strict URL Validation'),
'description': _('Require schema specification when validating URLs'),
'validator': bool,
'default': True,
'flags': [SettingFlag.SECURITY],
},
'INVENTREE_UPDATE_CHECK_INTERVAL': {
'name': _('Update Check Interval'),
@@ -364,12 +369,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable barcode scanner support in the web interface'),
'default': True,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'BARCODE_STORE_RESULTS': {
'name': _('Store Barcode Results'),
'description': _('Store barcode scan results in the database'),
'default': False,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'BARCODE_RESULTS_MAX_NUM': {
'name': _('Barcode Scans Maximum Count'),
@@ -389,12 +396,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Allow barcode scanning via webcam in browser'),
'default': True,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'BARCODE_SHOW_TEXT': {
'name': _('Barcode Show Data'),
'description': _('Display barcode data in browser as text'),
'default': False,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'BARCODE_GENERATION_PLUGIN': {
'name': _('Barcode Generation Plugin'),
@@ -407,12 +416,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable locking of parts to prevent modification'),
'validator': bool,
'default': True,
'flags': [SettingFlag.TOGGLE],
},
'PART_ENABLE_REVISION': {
'name': _('Part Revisions'),
'description': _('Enable revision field for Part'),
'validator': bool,
'default': True,
'flags': [SettingFlag.TOGGLE],
},
'PART_REVISION_ASSEMBLY_ONLY': {
'name': _('Assembly Revision Only'),
@@ -656,6 +667,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable label printing from the web interface'),
'default': True,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'LABEL_DPI': {
'name': _('Label Image DPI'),
@@ -670,24 +682,28 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable generation of reports'),
'default': False,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'REPORT_DEBUG_MODE': {
'name': _('Debug Mode'),
'description': _('Generate reports in debug mode (HTML output)'),
'default': False,
'validator': bool,
'flags': [SettingFlag.SECURITY],
},
'REPORT_FETCH_URLS': {
'name': _('Report URL Fetching'),
'description': _('Allow fetching of remote URLs when generating reports'),
'default': False,
'validator': bool,
'flags': [SettingFlag.SECURITY],
},
'REPORT_LOG_ERRORS': {
'name': _('Log Report Errors'),
'description': _('Log errors which occur when generating reports'),
'default': False,
'validator': bool,
'flags': [SettingFlag.SECURITY],
},
'REPORT_DEFAULT_PAGE_SIZE': {
'name': _('Page Size'),
@@ -737,6 +753,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable stock expiry functionality'),
'default': False,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'STOCK_ALLOW_EXPIRED_SALE': {
'name': _('Sell Expired Stock'),
@@ -764,6 +781,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable ownership control over stock locations and items'),
'default': False,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'STOCK_LOCATION_DEFAULT_ICON': {
'name': _('Stock Location Default Icon'),
@@ -866,6 +884,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable return order functionality in the user interface'),
'validator': bool,
'default': False,
'flags': [SettingFlag.TOGGLE],
},
'RETURNORDER_REFERENCE_PATTERN': {
'name': _('Return Order Reference Pattern'),
@@ -936,6 +955,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable transfer order functionality in the user interface'),
'validator': bool,
'default': False,
'flags': [SettingFlag.TOGGLE],
},
'TRANSFERORDER_REFERENCE_PATTERN': {
'name': _('Transfer Order Reference Pattern'),
@@ -1017,18 +1037,21 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable password forgot function on the login pages'),
'default': True,
'validator': bool,
'flags': [SettingFlag.SECURITY],
},
'LOGIN_ENABLE_REG': {
'name': _('Enable registration'),
'description': _('Enable self-registration for users on the login pages'),
'default': False,
'validator': bool,
'flags': [SettingFlag.SECURITY],
},
'LOGIN_ENABLE_SSO': {
'name': _('Enable SSO'),
'description': _('Enable SSO on the login pages'),
'default': False,
'validator': bool,
'flags': [SettingFlag.SECURITY],
},
'LOGIN_ENABLE_SSO_REG': {
'name': _('Enable SSO registration'),
@@ -1037,6 +1060,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
),
'default': False,
'validator': bool,
'flags': [SettingFlag.SECURITY],
},
'LOGIN_ENABLE_SSO_GROUP_SYNC': {
'name': _('Enable SSO group sync'),
@@ -1045,12 +1069,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
),
'default': False,
'validator': bool,
'flags': [SettingFlag.SECURITY],
},
'SSO_GROUP_KEY': {
'name': _('SSO group key'),
'description': _('The name of the groups claim attribute provided by the IdP'),
'default': 'groups',
'validator': str,
'flags': [SettingFlag.SECURITY],
},
'SSO_GROUP_MAP': {
'name': _('SSO group map'),
@@ -1058,6 +1084,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'A mapping from SSO groups to local InvenTree groups. If the local group does not exist, it will be created.'
),
'validator': json.loads,
'flags': [SettingFlag.SECURITY],
'default': '{}',
},
'SSO_REMOVE_GROUPS': {
@@ -1067,6 +1094,11 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
),
'default': True,
'validator': bool,
'confirm': True,
'confirm_text': _(
'Disabling this setting will stop cleaning up groups that external users no longer belong to. This might cause security issues.'
),
'flags': [SettingFlag.SECURITY],
},
'LOGIN_MAIL_REQUIRED': {
'name': _('Email required'),
@@ -1099,6 +1131,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
),
'default': '',
'before_save': common.validators.validate_email_domains,
'flags': [SettingFlag.SECURITY],
},
'SIGNUP_GROUP': {
'name': _('Group on signup'),
@@ -1107,6 +1140,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
),
'default': '',
'choices': settings_group_options,
'flags': [SettingFlag.SECURITY],
},
'LOGIN_ENFORCE_MFA': {
'name': _('Enforce MFA'),
@@ -1117,6 +1151,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'confirm_text': _(
'Enabling this setting will require all users to set up multifactor authentication. All sessions will be disconnected immediately.'
),
'flags': [SettingFlag.SECURITY],
'after_save': enforce_mfa,
},
'PLUGIN_ON_STARTUP': {
@@ -1141,6 +1176,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
'after_save': reload_plugin_registry,
'flags': [SettingFlag.TOGGLE],
},
'ENABLE_PLUGINS_NAVIGATION': {
'name': _('Enable navigation integration'),
@@ -1148,6 +1184,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
'after_save': reload_plugin_registry,
'flags': [SettingFlag.TOGGLE],
},
'ENABLE_PLUGINS_APP': {
'name': _('Enable app integration'),
@@ -1155,6 +1192,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
'after_save': reload_plugin_registry,
'flags': [SettingFlag.TOGGLE],
},
'ENABLE_PLUGINS_SCHEDULE': {
'name': _('Enable schedule integration'),
@@ -1162,6 +1200,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
'after_save': reload_plugin_registry,
'flags': [SettingFlag.TOGGLE],
},
'ENABLE_PLUGINS_EVENTS': {
'name': _('Enable event integration'),
@@ -1169,6 +1208,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
'after_save': reload_plugin_registry,
'flags': [SettingFlag.TOGGLE],
},
'ENABLE_PLUGINS_INTERFACE': {
'name': _('Enable interface integration'),
@@ -1176,6 +1216,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
'after_save': reload_plugin_registry,
'flags': [SettingFlag.TOGGLE],
},
'ENABLE_PLUGINS_MAILS': {
'name': _('Enable mail integration'),
@@ -1183,18 +1224,21 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
'after_save': reload_plugin_registry,
'flags': [SettingFlag.TOGGLE],
},
'PROJECT_CODES_ENABLED': {
'name': _('Enable project codes'),
'description': _('Enable project codes for tracking projects'),
'default': False,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'STOCKTAKE_ENABLE': {
'name': _('Enable Stocktake'),
'description': _(
'Enable functionality for recording historical stock levels and value'
),
'flags': [SettingFlag.TOGGLE],
'validator': bool,
'default': False,
},
@@ -1287,6 +1331,7 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'description': _('Enable test station data collection for test results'),
'default': False,
'validator': bool,
'flags': [SettingFlag.TOGGLE],
},
'MACHINE_PING_ENABLED': {
'name': _('Enable Machine Ping'),
@@ -1296,4 +1341,25 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': True,
'validator': bool,
},
'SALESORDER_ENABLED': {
'name': _('Enable Sales Orders'),
'description': _('Enable sales order functionality in the user interface'),
'validator': bool,
'default': True,
'flags': [SettingFlag.TOGGLE],
},
'PURCHASEORDER_ENABLED': {
'name': _('Enable Purchase Orders'),
'description': _('Enable purchase order functionality in the user interface'),
'validator': bool,
'default': True,
'flags': [SettingFlag.TOGGLE],
},
'BUILDORDER_ENABLED': {
'name': _('Enable Build Orders'),
'description': _('Enable build order functionality in the user interface'),
'validator': bool,
'default': True,
'flags': [SettingFlag.TOGGLE],
},
}
+25 -1
View File
@@ -1,7 +1,7 @@
"""Tests for the various validators in the settings."""
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.test import TestCase, override_settings
import common.setting.system
@@ -48,3 +48,27 @@ class SettingsValidatorTests(TestCase):
def test_update_instance_url_no_multi(self):
"""Test update_instance_url."""
self.assertIsNone(common.setting.system.update_instance_url('abc.com'))
@override_settings(SITE_URL=None)
def test_base_url_validator(self):
"""Test valid and invalid base URL values."""
validator = common.setting.system.BaseURLValidator()
for value in ['', 'http://localhost', 'https://inventree.example']:
self.assertIsNone(validator(value))
for value in ['inventree', 'ftp://inventree']:
with self.assertRaises(ValidationError):
validator(value)
@override_settings(SITE_URL='https://inventree.example')
def test_base_url_validator_locked_by_configuration(self):
"""Test that a configured site URL cannot be changed."""
validator = common.setting.system.BaseURLValidator()
self.assertIsNone(validator('https://inventree.example'))
with self.assertRaisesMessage(
ValidationError, 'Site URL is locked by configuration'
):
validator('https://other.example')
+33 -6
View File
@@ -1,7 +1,28 @@
"""Types for settings."""
from collections.abc import Callable
from typing import Any, NotRequired, TypedDict
from enum import StrEnum
# only import for type checking
from typing import TYPE_CHECKING, Any, NotRequired, Optional, TypedDict
if TYPE_CHECKING:
from django_stubs_ext import StrOrPromise
else:
StrOrPromise = str
# enum to mark what kind of behavior a setting might influence; these are not for enforcing a specific logic but mainly docs / warning messages
# these are NOT a security boundary
class SettingFlag(StrEnum):
"""Flags to indicate the behavior or purpose of a setting."""
"""Setting influences visibility or UI of major functionality."""
TOGGLE = 'org.inventree.settingsflag.function_toggle'
"""Setting is for internal use only and should not be exposed to end users."""
INTERNAL = 'org.inventree.settingsflag.internal'
"""Setting has security implications and should be handled with care."""
SECURITY = 'org.inventree.settingsflag.security'
class SettingsKeyType(TypedDict, total=False):
@@ -23,14 +44,19 @@ class SettingsKeyType(TypedDict, total=False):
model: Auto create a dropdown menu to select an associated model instance (e.g. 'company.company', 'auth.user' and 'auth.group' are possible too, optional)
confirm: Require an explicit confirmation before changing the setting (optional, default: False)
confirm_text: Text to display in the confirmation dialog (optional)
flags: List of SettingFlag indicating the behavior or purpose of the setting (optional)
"""
name: str
description: str
units: str
name: StrOrPromise
description: StrOrPromise
units: StrOrPromise
validator: Callable | list[Callable] | tuple[Callable]
default: Callable | Any
choices: list[tuple[str, str]] | Callable[[], list[tuple[str, str]]]
choices: (
list[tuple[str, StrOrPromise]]
| Callable[[], list[tuple[str, StrOrPromise]] | None]
| None
)
model_filters: dict[str, Any]
hidden: bool
before_save: Callable[..., None]
@@ -39,7 +65,8 @@ class SettingsKeyType(TypedDict, total=False):
required: bool
model: str
confirm: bool
confirm_text: str
confirm_text: StrOrPromise
flags: Optional[list[SettingFlag]]
class InvenTreeSettingsKeyType(SettingsKeyType):
+1
View File
@@ -459,6 +459,7 @@ class SettingsTest(InvenTreeTestCase):
'before_save',
'confirm',
'confirm_text',
'flags',
]
for k in setting:
+6 -1
View File
@@ -4,11 +4,16 @@ import base64
import io
import logging
import mimetypes
from typing import TYPE_CHECKING
from django.utils.translation import gettext_lazy as _
from common.settings import get_global_setting
if TYPE_CHECKING:
from django_stubs_ext import StrOrPromise
else:
StrOrPromise = str
logger = logging.getLogger('inventree')
@@ -38,7 +43,7 @@ def report_model_options():
]
def report_page_size_options():
def report_page_size_options() -> list[tuple[str, StrOrPromise]]:
"""Returns a list of page size options for PDF reports."""
return [
('A4', _('A4')),
+22 -27
View File
@@ -46,6 +46,28 @@ User.add_to_class('__str__', user_model_str) # Overriding User.__str__
# OVERRIDE END
if settings.LDAP_AUTH: # pragma: no cover
from django_auth_ldap.backend import populate_user # ty: ignore[unresolved-import]
@receiver(populate_user)
def create_email_address(user, **kwargs):
"""If a django user is from LDAP and has an email attached to it, create an allauth email address for them automatically.
https://django-auth-ldap.readthedocs.io/en/latest/users.html#populating-users
https://django-auth-ldap.readthedocs.io/en/latest/reference.html#django_auth_ldap.backend.populate_user
"""
# User must exist in the database before we can create their EmailAddress. By their recommendation,
# we can just call .save() now
user.save()
# if they got an email address from LDAP, create it now and make it the primary
if (
user.email
and not EmailAddress.objects.filter(user=user, email=user.email).exists()
):
EmailAddress.objects.create(user=user, email=user.email, primary=True)
def default_token():
"""Generate a default value for the token."""
return ApiToken.generate_key()
@@ -625,30 +647,3 @@ def validate_primary_group_on_group_change(sender, instance, action, **kwargs):
if profile.primary_group and profile.primary_group not in instance.groups.all():
profile.primary_group = None
profile.save()
# update allauth user mail
@receiver(post_save, sender=User)
def sync_user_email_address(sender, instance: User, created: bool, **kwargs):
"""Keep the allauth EmailAddress in sync with User email field."""
# Are we currently in the API path of user registration?
if getattr(instance, '_is_registering', False):
return
if isImportingData() or isReadOnlyCommand():
return
if not instance.email:
return
primary_address = EmailAddress.objects.filter(user=instance, primary=True).first()
if primary_address:
if primary_address.email != instance.email:
primary_address.email = instance.email
primary_address.verified = False
primary_address.save()
elif not EmailAddress.objects.filter(user=instance, email=instance.email).exists():
EmailAddress.objects.create(
user=instance, email=instance.email, primary=True, verified=False
)
@@ -514,6 +514,8 @@ class UserCreateSerializer(ExtendedUserSerializer):
def create(self, validated_data):
"""Send an e email to the user after creation."""
from allauth.account.models import EmailAddress
from InvenTree.helpers_model import get_base_url
from InvenTree.tasks import email_user, offload_task
@@ -521,6 +523,12 @@ class UserCreateSerializer(ExtendedUserSerializer):
instance = super().create(validated_data)
# Create the EmailAddress entry for the user
if instance.email:
EmailAddress.objects.create(
user=instance, email=instance.email, primary=True, verified=False
)
# Make sure the user cannot login until they have set a password
instance.set_unusable_password()
+5 -20
View File
@@ -125,6 +125,11 @@ class UserAPITests(InvenTreeAPITestCase):
self.assertEqual(response.data['is_staff'], False)
self.assertEqual(response.data['is_superuser'], False)
self.assertEqual(response.data['is_active'], True)
self.assertTrue(
EmailAddress.objects.filter(
user__username=data['username'], email=data['email'], primary=True
).exists()
)
# Try to adjust the 'is_superuser' field
# Only a "superuser" can set this field
@@ -372,26 +377,6 @@ class SuperuserAPITests(InvenTreeAPITestCase):
resp = self.put(url, {'password': 'inventree'}, expected_code=200)
self.assertEqual(resp.data, {})
def test_email_address_sync_signal(self):
"""Test emailadress sync."""
user = User.objects.create(username='start', email='start@example.org')
self.assertTrue(
EmailAddress.objects.filter(
user=user, email='start@example.org', primary=True
).exists()
)
# change should trigger emailaddress update
user.email = 'updated@example.org'
user.save()
self.assertFalse(
EmailAddress.objects.filter(user=user, email='start@example.org').exists()
)
updated = EmailAddress.objects.get(user=user, primary=True)
self.assertEqual(updated.email, 'updated@example.org')
self.assertFalse(updated.verified)
class UserTokenTests(InvenTreeAPITestCase):
"""Tests for user token functionality."""
+8
View File
@@ -53,3 +53,11 @@ export function userRoleLabel(role: UserRoles): string {
return role as string;
}
}
export const roleToViewSettingMap: Partial<Record<UserRoles, string>> = {
[UserRoles.build]: 'BUILDORDER_ENABLED',
[UserRoles.sales_order]: 'SALESORDER_ENABLED',
[UserRoles.purchase_order]: 'PURCHASEORDER_ENABLED',
[UserRoles.transfer_order]: 'TRANSFERORDER_ENABLED',
[UserRoles.return_order]: 'RETURNORDER_ENABLED'
};
+7
View File
@@ -16,6 +16,12 @@ export enum SettingType {
Model = 'related field'
}
export enum SettingFlag {
TOGGLE = 'org.inventree.settingsflag.function_toggle',
INTERNAL = 'org.inventree.settingsflag.internal',
SECURITY = 'org.inventree.settingsflag.security'
}
// Type interface defining a single 'setting' object
export interface Setting {
pk: number;
@@ -36,6 +42,7 @@ export interface Setting {
read_only?: boolean;
confirm?: boolean;
confirm_text?: string;
flags: SettingFlag[];
}
export interface SettingChoice {
+1
View File
@@ -47,6 +47,7 @@ export interface UserStateProps {
hasChangeRole: (role: UserRoles) => boolean;
hasAddRole: (role: UserRoles) => boolean;
hasViewRole: (role: UserRoles) => boolean;
hasViewVisible: (role: UserRoles) => boolean;
checkUserPermission: (
model: ModelType,
permission: UserPermissions
@@ -83,21 +83,21 @@ function DrawerContent({ closeFunc }: Readonly<{ closeFunc?: () => void }>) {
id: 'build',
title: t`Manufacturing`,
link: '/manufacturing/',
hidden: !user.hasViewRole(UserRoles.build),
hidden: !user.hasViewVisible(UserRoles.build),
icon: 'build'
},
{
id: 'purchasing',
title: t`Purchasing`,
link: '/purchasing/',
hidden: !user.hasViewRole(UserRoles.purchase_order),
hidden: !user.hasViewVisible(UserRoles.purchase_order),
icon: 'purchase_orders'
},
{
id: 'sales',
title: t`Sales`,
link: '/sales/',
hidden: !user.hasViewRole(UserRoles.sales_order),
hidden: !user.hasViewVisible(UserRoles.sales_order),
icon: 'sales_orders'
},
{
@@ -306,7 +306,7 @@ export function SearchDrawer({
part_detail: true
},
enabled:
user.hasViewRole(UserRoles.build) &&
user.hasViewVisible(UserRoles.build) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_BUILD_ORDERS')
},
{
@@ -316,7 +316,7 @@ export function SearchDrawer({
title: t`Suppliers`,
parameters: {},
enabled:
user.hasViewRole(UserRoles.purchase_order) &&
user.hasViewVisible(UserRoles.purchase_order) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_COMPANIES')
},
{
@@ -326,7 +326,7 @@ export function SearchDrawer({
title: t`Manufacturers`,
parameters: {},
enabled:
user.hasViewRole(UserRoles.purchase_order) &&
user.hasViewVisible(UserRoles.purchase_order) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_COMPANIES')
},
{
@@ -336,7 +336,7 @@ export function SearchDrawer({
title: t`Customers`,
parameters: {},
enabled:
user.hasViewRole(UserRoles.sales_order) &&
user.hasViewVisible(UserRoles.sales_order) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_COMPANIES')
},
{
@@ -350,7 +350,7 @@ export function SearchDrawer({
: undefined
},
enabled:
user.hasViewRole(UserRoles.purchase_order) &&
user.hasViewVisible(UserRoles.purchase_order) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS')
},
{
@@ -364,14 +364,14 @@ export function SearchDrawer({
: undefined
},
enabled:
user.hasViewRole(UserRoles.sales_order) &&
user.hasViewVisible(UserRoles.sales_order) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_SALES_ORDERS')
},
{
model: ModelType.salesordershipment,
parameters: {},
enabled:
user.hasViewRole(UserRoles.sales_order) &&
user.hasViewVisible(UserRoles.sales_order) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_SALES_ORDER_SHIPMENTS')
},
{
@@ -385,7 +385,7 @@ export function SearchDrawer({
: undefined
},
enabled:
user.hasViewRole(UserRoles.return_order) &&
user.hasViewVisible(UserRoles.return_order) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_RETURN_ORDERS')
}
];
@@ -10,14 +10,14 @@ import {
Tooltip,
useMantineColorScheme
} from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { IconEdit, IconInfoCircle } from '@tabler/icons-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Boundary } from '@lib/components/Boundary';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import type { Setting } from '@lib/types/Settings';
import { type Setting, SettingFlag } from '@lib/types/Settings';
import { api } from '../../App';
import { vars } from '../../theme';
import { RenderInstance } from '../render/Instance';
@@ -205,6 +205,23 @@ export function SettingItem({
colorScheme === 'light' ? vars.colors.gray[1] : vars.colors.gray[9];
}
const [flagText, flagColor] = useMemo(() => {
if (!setting.flags || setting.flags.length === 0) {
return ['', ''];
}
if (setting.flags.includes(SettingFlag.SECURITY)) {
return [t`Security relevant setting`, vars.colors.red[7]];
}
if (setting.flags.includes(SettingFlag.TOGGLE)) {
return [
t`Function Toggle - effects system behavior and/or feature visibility`,
vars.colors.blue[7]
];
}
return ['', ''];
}, [setting.flags]);
return (
<Paper style={style}>
<Group justify='space-between' p='3'>
@@ -222,6 +239,11 @@ export function SettingItem({
<IconEdit color={vars.colors.yellow[7]} size={16} />
</Tooltip>
)}
{flagText && (
<Tooltip label={flagText}>
<IconInfoCircle color={flagColor} size={16} />
</Tooltip>
)}
<SettingValue
setting={setting}
onEdit={onEdit}
+5 -7
View File
@@ -127,7 +127,7 @@ export function getActions(navigate: NavigateFunction) {
});
// Page Actions
user?.hasViewRole(UserRoles.purchase_order) &&
user?.hasViewVisible(UserRoles.purchase_order) &&
_actions.push({
id: 'purchase-orders',
label: t`Purchase Orders`,
@@ -137,7 +137,7 @@ export function getActions(navigate: NavigateFunction) {
leftSection: <IconLink size='1.2rem' />
});
user?.hasViewRole(UserRoles.sales_order) &&
user?.hasViewVisible(UserRoles.sales_order) &&
_actions.push({
id: 'sales-orders',
label: t`Sales Orders`,
@@ -147,8 +147,7 @@ export function getActions(navigate: NavigateFunction) {
leftSection: <IconLink size='1.2rem' />
});
globalSettings.isSet('TRANSFERORDER_ENABLED') &&
user?.hasViewRole(UserRoles.transfer_order) &&
user?.hasViewVisible(UserRoles.transfer_order) &&
_actions.push({
id: 'transfer-orders',
label: t`Transfer Orders`,
@@ -158,8 +157,7 @@ export function getActions(navigate: NavigateFunction) {
leftSection: <IconLink size='1.2rem' />
});
globalSettings.isSet('RETURNORDER_ENABLED') &&
user?.hasViewRole(UserRoles.return_order) &&
user?.hasViewVisible(UserRoles.return_order) &&
_actions.push({
id: 'return-orders',
label: t`Return Orders`,
@@ -178,7 +176,7 @@ export function getActions(navigate: NavigateFunction) {
leftSection: <IconBarcode size='1.2rem' />
});
user?.hasViewRole(UserRoles.build) &&
user?.hasViewVisible(UserRoles.build) &&
_actions.push({
id: 'builds',
label: t`Build Orders`,
+5 -7
View File
@@ -49,29 +49,27 @@ export function getNavTabs(user: UserStateProps): NavTab[] {
visible:
user.hasViewRole(UserRoles.stock) ||
user.hasViewRole(UserRoles.stock_location) ||
(globalSettings.isSet('TRANSFERORDER_ENABLED') &&
user.hasViewRole(UserRoles.transfer_order))
user.hasViewVisible(UserRoles.transfer_order)
},
{
name: 'manufacturing',
title: t`Manufacturing`,
icon: <IconBuildingFactory2 />,
visible: user.hasViewRole(UserRoles.build)
visible: user.hasViewVisible(UserRoles.build)
},
{
name: 'purchasing',
title: t`Purchasing`,
icon: <IconShoppingCart />,
visible: user.hasViewRole(UserRoles.purchase_order)
visible: user.hasViewVisible(UserRoles.purchase_order)
},
{
name: 'sales',
title: t`Sales`,
icon: <IconTruckDelivery />,
visible:
user.hasViewRole(UserRoles.sales_order) ||
(globalSettings.isSet('RETURNORDER_ENABLED') &&
user.hasViewRole(UserRoles.return_order))
user.hasViewVisible(UserRoles.sales_order) ||
user.hasViewVisible(UserRoles.return_order)
}
];
@@ -14,7 +14,6 @@ import {
IconQrcode,
IconServerCog,
IconShoppingCart,
IconTransfer,
IconTruckDelivery
} from '@tabler/icons-react';
import { useMemo } from 'react';
@@ -303,6 +302,15 @@ export default function SystemSettings() {
'STOCK_TRACKING_DELETE_DAYS'
]}
/>
<GlobalSettingList
heading={t`Transfer Orders`}
keys={[
'TRANSFERORDER_ENABLED',
'TRANSFERORDER_REFERENCE_PATTERN',
'TRANSFERORDER_REQUIRE_RESPONSIBLE',
'TRANSFERORDER_EDIT_COMPLETED_ORDERS'
]}
/>
</Stack>
)
},
@@ -315,6 +323,7 @@ export default function SystemSettings() {
<GlobalSettingList
heading={t`Build Orders`}
keys={[
'BUILDORDER_ENABLED',
'BUILDORDER_REFERENCE_PATTERN',
'BUILDORDER_REQUIRE_RESPONSIBLE',
'BUILDORDER_REQUIRE_ACTIVE_PART',
@@ -342,6 +351,7 @@ export default function SystemSettings() {
<GlobalSettingList
heading={t`Purchase Orders`}
keys={[
'PURCHASEORDER_ENABLED',
'PURCHASEORDER_REFERENCE_PATTERN',
'PURCHASEORDER_REQUIRE_RESPONSIBLE',
'PURCHASEORDER_CONVERT_CURRENCY',
@@ -360,6 +370,7 @@ export default function SystemSettings() {
<GlobalSettingList
heading={t`Sales Orders`}
keys={[
'SALESORDER_ENABLED',
'SALESORDER_REFERENCE_PATTERN',
'SALESORDER_REQUIRE_RESPONSIBLE',
'SALESORDER_DEFAULT_SHIPMENT',
@@ -381,21 +392,6 @@ export default function SystemSettings() {
</Stack>
)
},
{
name: 'transferorders',
label: t`Transfer Orders`,
icon: <IconTransfer />,
content: (
<GlobalSettingList
keys={[
'TRANSFERORDER_ENABLED',
'TRANSFERORDER_REFERENCE_PATTERN',
'TRANSFERORDER_REQUIRE_RESPONSIBLE',
'TRANSFERORDER_EDIT_COMPLETED_ORDERS'
]}
/>
)
},
{
name: 'plugins',
label: t`Plugins`,
+1 -1
View File
@@ -335,7 +335,7 @@ export default function BuildDetail() {
<Skeleton />
),
hidden:
!user.hasViewRole(UserRoles.purchase_order) ||
!user.hasViewVisible(UserRoles.purchase_order) ||
!build.external ||
!globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS')
},
+1 -1
View File
@@ -112,7 +112,7 @@ export default function BuildIndex() {
];
}, [user, buildOrderView]);
if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.build)) {
if (!user.isLoggedIn() || !user.hasViewVisible(UserRoles.build)) {
return <PermissionDenied />;
}
@@ -108,14 +108,18 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
name: 'supplied-parts',
label: t`Supplied Parts`,
icon: <IconPackageExport />,
hidden: !company?.is_supplier,
hidden:
!company?.is_supplier ||
!user?.hasViewVisible(UserRoles.purchase_order),
content: company?.pk && <SupplierPartTable supplierId={company.pk} />
},
{
name: 'manufactured-parts',
label: t`Manufactured Parts`,
icon: <IconBuildingWarehouse />,
hidden: !company?.is_manufacturer,
hidden:
!company?.is_manufacturer ||
!user?.hasViewVisible(UserRoles.purchase_order),
content: company?.pk && (
<ManufacturerPartTable manufacturerId={company.pk} />
)
@@ -124,7 +128,9 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
name: 'purchase-orders',
label: t`Purchase Orders`,
icon: <IconShoppingCart />,
hidden: !company?.is_supplier,
hidden:
!company?.is_supplier ||
!user?.hasViewVisible(UserRoles.purchase_order),
content: company?.pk && <PurchaseOrderTable supplierId={company.pk} />
},
{
@@ -144,14 +150,17 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
name: 'sales-orders',
label: t`Sales Orders`,
icon: <IconTruckDelivery />,
hidden: !company?.is_customer,
hidden:
!company?.is_customer || !user?.hasViewVisible(UserRoles.sales_order),
content: company?.pk && <SalesOrderTable customerId={company.pk} />
},
{
name: 'return-orders',
label: t`Return Orders`,
icon: <IconTruckReturn />,
hidden: !company?.is_customer,
hidden:
!company?.is_customer ||
!user?.hasViewVisible(UserRoles.return_order),
content: company.pk ? (
<ReturnOrderTable customerId={company.pk} />
) : (
@@ -15,7 +15,7 @@ export default function PartAllocationPanel({ part }: Readonly<{ part: any }>) {
multiple={true}
defaultValue={['buildallocations', 'salesallocations']}
>
{part.component && user.hasViewRole(UserRoles.build) && (
{part.component && user.hasViewVisible(UserRoles.build) && (
<Accordion.Item value='buildallocations' key='buildallocations'>
<Accordion.Control>
<StylishText size='lg'>{t`Build Order Allocations`}</StylishText>
@@ -25,7 +25,7 @@ export default function PartAllocationPanel({ part }: Readonly<{ part: any }>) {
</Accordion.Panel>
</Accordion.Item>
)}
{part.salable && user.hasViewRole(UserRoles.sales_order) && (
{part.salable && user.hasViewVisible(UserRoles.sales_order) && (
<Accordion.Item value='salesallocations' key='salesallocations'>
<Accordion.Control>
<StylishText size='lg'>{t`Sales Order Allocations`}</StylishText>
+6 -12
View File
@@ -379,7 +379,7 @@ export default function PartDetail() {
label: t`Suppliers`,
icon: <IconBuilding />,
hidden:
!part.purchaseable || !user.hasViewRole(UserRoles.purchase_order),
!part.purchaseable || !user.hasViewVisible(UserRoles.purchase_order),
content: part.pk ? (
<PartSupplierDetail partId={part.pk} />
@@ -392,7 +392,7 @@ export default function PartDetail() {
label: t`Purchase Orders`,
icon: <IconShoppingCart />,
hidden:
!part.purchaseable || !user.hasViewRole(UserRoles.purchase_order),
!part.purchaseable || !user.hasViewVisible(UserRoles.purchase_order),
content: part.pk ? (
<PartPurchaseOrdersTable partId={part.pk} />
) : (
@@ -403,7 +403,7 @@ export default function PartDetail() {
name: 'sales_orders',
label: t`Sales Orders`,
icon: <IconTruckDelivery />,
hidden: !part.salable || !user.hasViewRole(UserRoles.sales_order),
hidden: !part.salable || !user.hasViewVisible(UserRoles.sales_order),
content: part.pk ? (
<PartSalesOrdersTable partId={part.pk} />
) : (
@@ -414,27 +414,21 @@ export default function PartDetail() {
name: 'return_orders',
label: t`Return Orders`,
icon: <IconTruckReturn />,
hidden:
!part.salable ||
!user.hasViewRole(UserRoles.return_order) ||
!globalSettings.isSet('RETURNORDER_ENABLED'),
hidden: !part.salable || !user.hasViewVisible(UserRoles.return_order),
content: part.pk ? <ReturnOrderTable partId={part.pk} /> : <Skeleton />
},
{
name: 'builds',
label: t`Build Orders`,
icon: <IconTools />,
hidden: !part.assembly || !user.hasViewRole(UserRoles.build),
hidden: !part.assembly || !user.hasViewVisible(UserRoles.build),
content: part.pk ? <BuildOrderTable partId={part.pk} /> : <Skeleton />
},
{
name: 'transfer_orders',
label: t`Transfer Orders`,
icon: <IconTransfer />,
hidden:
part.virtual ||
!globalSettings.isSet('TRANSFERORDER_ENABLED') ||
!user.hasViewRole(UserRoles.transfer_order),
hidden: part.virtual || !user.hasViewVisible(UserRoles.transfer_order),
content: part.pk ? (
<TransferOrderTable partId={part.pk} />
) : (
@@ -46,11 +46,11 @@ export default function PartPricingPanel({ part }: Readonly<{ part: any }>) {
}, [globalSettings]);
const purchaseOrderPricing = useMemo(() => {
return user.hasViewRole(UserRoles.purchase_order) && part?.purchaseable;
return user.hasViewVisible(UserRoles.purchase_order) && part?.purchaseable;
}, [user, part]);
const salesOrderPricing = useMemo(() => {
return user.hasViewRole(UserRoles.sales_order) && part?.salable;
return user.hasViewVisible(UserRoles.sales_order) && part?.salable;
}, [user, part]);
const [value, setValue] = useState<string[]>([panelOptions.overview]);
@@ -94,7 +94,7 @@ export default function PurchasingIndex() {
name: 'purchaseorders',
label: t`Purchase Orders`,
icon: <IconShoppingCart />,
hidden: !user.hasViewRole(UserRoles.purchase_order),
hidden: !user.hasViewVisible(UserRoles.purchase_order),
selection: purchaseOrderView,
onChange: setPurchaseOrderView,
options: [
@@ -228,7 +228,7 @@ export default function PurchasingIndex() {
supplierView
]);
if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.purchase_order)) {
if (!user.isLoggedIn() || !user.hasViewVisible(UserRoles.purchase_order)) {
return <PermissionDenied />;
}
+5 -2
View File
@@ -22,6 +22,7 @@ import PermissionDenied from '../../components/errors/PermissionDenied';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/panels/PanelGroup';
import SegmentedControlPanel from '../../components/panels/SegmentedControlPanel';
import { useGlobalSettingsState } from '../../states/SettingsStates';
import { useUserState } from '../../states/UserState';
import { CompanyTable } from '../../tables/company/CompanyTable';
import ParametricCompanyTable from '../../tables/company/ParametricCompanyTable';
@@ -84,6 +85,7 @@ const ReturnOrderCalendar = () => {
export default function SalesIndex() {
const user = useUserState();
const globalSettings = useGlobalSettingsState();
const [customersView, setCustomersView] = useLocalStorage<string>({
key: 'customer-view',
@@ -106,7 +108,7 @@ export default function SalesIndex() {
name: 'salesorders',
label: t`Sales Orders`,
icon: <IconTruckDelivery />,
hidden: !user.hasViewRole(UserRoles.sales_order),
hidden: !user.hasViewVisible(UserRoles.sales_order),
selection: salesOrderView,
onChange: setSalesOrderView,
options: [
@@ -134,6 +136,7 @@ export default function SalesIndex() {
name: 'shipments',
label: t`Pending Shipments`,
icon: <IconCubeSend />,
hidden: !user.hasViewVisible(UserRoles.sales_order),
content: (
<SalesOrderShipmentTable
tableName={'sales-order-pending-shipment'}
@@ -146,7 +149,7 @@ export default function SalesIndex() {
name: 'returnorders',
label: t`Return Orders`,
icon: <IconTruckReturn />,
hidden: !user.hasViewRole(UserRoles.return_order),
hidden: !user.hasViewVisible(UserRoles.return_order),
selection: returnOrderView,
onChange: setReturnOrderView,
options: [
@@ -220,7 +220,7 @@ export default function SalesOrderDetail() {
name: 'build-orders',
label: t`Build Orders`,
icon: <IconTools />,
hidden: !user.hasViewRole(UserRoles.build),
hidden: !user.hasViewVisible(UserRoles.build),
content: order?.pk ? (
<BuildOrderTable salesOrderId={order.pk} />
) : (
@@ -170,9 +170,7 @@ export default function Stock() {
name: 'transfer-orders',
label: t`Transfer Orders`,
icon: <IconTransfer />,
hidden:
!user.hasViewRole(UserRoles.transfer_order) ||
!globalSettings.isSet('TRANSFERORDER_ENABLED'),
hidden: !user.hasViewVisible(UserRoles.transfer_order),
selection: transferOrderView,
onChange: setTransferOrderView,
options: [
+1 -1
View File
@@ -134,7 +134,7 @@ export default function StockDetail() {
const showTransferAllocations: boolean = useMemo(() => {
return (
!stockitem?.part_detail?.virtual &&
globalSettings.isSet('TRANSFERORDER_ENABLED')
globalSettings.isSet('TRANSFERORDER_ENABLED') // todo check if role is available
);
}, [stockitem]);
+16 -1
View File
@@ -2,12 +2,17 @@ import { create } from 'zustand';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import type { ModelType } from '@lib/enums/ModelType';
import { UserPermissions, type UserRoles } from '@lib/enums/Roles';
import {
UserPermissions,
type UserRoles,
roleToViewSettingMap
} from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import type { UserProps, UserStateProps } from '@lib/types/User';
import { api, setApiDefaults } from '../App';
import { clearCsrfCookie } from '../functions/auth';
import { useServerApiState } from './ServerApiState';
import { useGlobalSettingsState } from './SettingsStates';
/**
* Global user information state, using Zustand manager
@@ -154,6 +159,16 @@ export const useUserState = create<UserStateProps>((set, get) => ({
hasViewRole: (role: UserRoles) => {
return get().checkUserRole(role, UserPermissions.view);
},
hasViewVisible: (role: UserRoles) => {
if (!get().hasViewRole(role)) {
return false;
}
const viewSetting = roleToViewSettingMap[role];
return viewSetting
? useGlobalSettingsState.getState().isSet(viewSetting)
: true;
},
checkUserPermission: (model: ModelType, permission: UserPermissions) => {
// Check if the user has the specified permission for the specified model
const user: UserProps = get().user as UserProps;
@@ -125,7 +125,7 @@ export default function PartBuildAllocationsTable({
title: t`View Build Order`,
modelType: ModelType.build,
modelId: record.build,
hidden: !user.hasViewRole(UserRoles.build),
hidden: !user.hasViewVisible(UserRoles.build),
navigate: navigate
})
];
@@ -91,7 +91,7 @@ export default function PartSalesAllocationsTable({
title: t`View Sales Order`,
modelType: ModelType.salesorder,
modelId: record.order,
hidden: !user.hasViewRole(UserRoles.sales_order),
hidden: !user.hasViewVisible(UserRoles.sales_order),
navigate: navigate
})
];
@@ -597,7 +597,7 @@ export default function SalesOrderLineItemTable({
modelType: ModelType.part,
modelId: record.part,
navigate: navigate,
hidden: !user.hasViewRole(UserRoles.part)
hidden: !user.hasViewVisible(UserRoles.part)
})
];
},
@@ -263,7 +263,7 @@ export default function SalesOrderShipmentTable({
hidden:
!record.order ||
!showOrderInfo ||
!user.hasViewRole(UserRoles.sales_order),
!user.hasViewVisible(UserRoles.sales_order),
navigate: navigate
})
];