mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-28 14:45:57 +00:00
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:
@@ -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") }}
|
||||
|
||||
@@ -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") }}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
site_obj.name = setting.value
|
||||
site_obj.save()
|
||||
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()
|
||||
site_obj.domain = setting.value
|
||||
site_obj.save()
|
||||
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],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -459,6 +459,7 @@ class SettingsTest(InvenTreeTestCase):
|
||||
'before_save',
|
||||
'confirm',
|
||||
'confirm_text',
|
||||
'flags',
|
||||
]
|
||||
|
||||
for k in setting:
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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')
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user