diff --git a/src/backend/InvenTree/InvenTree/apps.py b/src/backend/InvenTree/InvenTree/apps.py index d4ecadeaf490..85d42bf18d27 100644 --- a/src/backend/InvenTree/InvenTree/apps.py +++ b/src/backend/InvenTree/InvenTree/apps.py @@ -14,6 +14,7 @@ import InvenTree.conversion import InvenTree.ready import InvenTree.tasks +from common.settings import get_global_setting, set_global_setting from InvenTree.config import get_setting logger = logging.getLogger('inventree') @@ -238,8 +239,6 @@ def update_site_url(self): - If a fixed SITE_URL is specified (via configuration), it should override the INVENTREE_BASE_URL setting - If multi-site support is enabled, update the site URL for the current site """ - import common.models - if not InvenTree.ready.canAppAccessDatabase(): return @@ -248,13 +247,8 @@ def update_site_url(self): if settings.SITE_URL: try: - if ( - common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL') - != settings.SITE_URL - ): - common.models.InvenTreeSetting.set_setting( - 'INVENTREE_BASE_URL', settings.SITE_URL - ) + if get_global_setting('INVENTREE_BASE_URL') != settings.SITE_URL: + set_global_setting('INVENTREE_BASE_URL', settings.SITE_URL) logger.info('Updated INVENTREE_SITE_URL to %s', settings.SITE_URL) except Exception: pass diff --git a/src/backend/InvenTree/InvenTree/fields.py b/src/backend/InvenTree/InvenTree/fields.py index 9dd0d81fb0a3..b1b276abb16f 100644 --- a/src/backend/InvenTree/InvenTree/fields.py +++ b/src/backend/InvenTree/InvenTree/fields.py @@ -14,6 +14,7 @@ from rest_framework.fields import empty import InvenTree.helpers +from common.settings import get_global_setting from .validators import AllowedURLValidator, allowable_url_schemes @@ -32,11 +33,7 @@ def __init__(self, **kwargs): def run_validation(self, data=empty): """Override default validation behaviour for this field type.""" - import common.models - - strict_urls = common.models.InvenTreeSetting.get_setting( - 'INVENTREE_STRICT_URLS', True, cache=False - ) + strict_urls = get_global_setting('INVENTREE_STRICT_URLS', True, cache=False) if not strict_urls and data is not empty and '://' not in data: # Validate as if there were a schema provided diff --git a/src/backend/InvenTree/InvenTree/forms.py b/src/backend/InvenTree/InvenTree/forms.py index 3358804a9ae2..ff4babe3bfe4 100644 --- a/src/backend/InvenTree/InvenTree/forms.py +++ b/src/backend/InvenTree/InvenTree/forms.py @@ -24,7 +24,7 @@ import InvenTree.helpers_model import InvenTree.sso -from common.models import InvenTreeSetting +from common.settings import get_global_setting from InvenTree.exceptions import log_error logger = logging.getLogger('inventree') @@ -172,12 +172,12 @@ class CustomSignupForm(SignupForm): def __init__(self, *args, **kwargs): """Check settings to influence which fields are needed.""" - kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED') + kwargs['email_required'] = get_global_setting('LOGIN_MAIL_REQUIRED') super().__init__(*args, **kwargs) # check for two mail fields - if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'): + if get_global_setting('LOGIN_SIGNUP_MAIL_TWICE'): self.fields['email2'] = forms.EmailField( label=_('Email (again)'), widget=forms.TextInput( @@ -189,7 +189,7 @@ def __init__(self, *args, **kwargs): ) # check for two password fields - if not InvenTreeSetting.get_setting('LOGIN_SIGNUP_PWD_TWICE'): + if not get_global_setting('LOGIN_SIGNUP_PWD_TWICE'): self.fields.pop('password2') # reorder fields @@ -202,7 +202,7 @@ def clean(self): cleaned_data = super().clean() # check for two mail fields - if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'): + if get_global_setting('LOGIN_SIGNUP_MAIL_TWICE'): email = cleaned_data.get('email') email2 = cleaned_data.get('email2') if (email and email2) and email != email2: @@ -213,10 +213,7 @@ def clean(self): def registration_enabled(): """Determine whether user registration is enabled.""" - if ( - InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') - or InvenTree.sso.registration_enabled() - ): + if get_global_setting('LOGIN_ENABLE_REG') or InvenTree.sso.registration_enabled(): if settings.EMAIL_HOST: return True else: @@ -240,9 +237,7 @@ def is_open_for_signup(self, request, *args, **kwargs): def clean_email(self, email): """Check if the mail is valid to the pattern in LOGIN_SIGNUP_MAIL_RESTRICTION (if enabled in settings).""" - mail_restriction = InvenTreeSetting.get_setting( - 'LOGIN_SIGNUP_MAIL_RESTRICTION', None - ) + mail_restriction = get_global_setting('LOGIN_SIGNUP_MAIL_RESTRICTION', None) if not mail_restriction: return super().clean_email(email) @@ -273,7 +268,7 @@ def save_user(self, request, user, form, commit=True): user = super().save_user(request, user, form) # Check if a default group is set in settings - start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP') + start_group = get_global_setting('SIGNUP_GROUP') if start_group: try: group = Group.objects.get(id=start_group) @@ -333,7 +328,7 @@ class CustomSocialAccountAdapter( def is_auto_signup_allowed(self, request, sociallogin): """Check if auto signup is enabled in settings.""" - if InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO', True): + if get_global_setting('LOGIN_SIGNUP_SSO_AUTO', True): return super().is_auto_signup_allowed(request, sociallogin) return False @@ -385,7 +380,7 @@ class CustomRegisterSerializer(RegisterSerializer): def __init__(self, instance=None, data=..., **kwargs): """Check settings to influence which fields are needed.""" - kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED') + kwargs['email_required'] = get_global_setting('LOGIN_MAIL_REQUIRED') super().__init__(instance, data, **kwargs) def save(self, request): diff --git a/src/backend/InvenTree/InvenTree/helpers_model.py b/src/backend/InvenTree/InvenTree/helpers_model.py index 4296b4f0dcc4..3fb73c852d2c 100644 --- a/src/backend/InvenTree/InvenTree/helpers_model.py +++ b/src/backend/InvenTree/InvenTree/helpers_model.py @@ -15,7 +15,6 @@ from djmoney.money import Money from PIL import Image -import common.models import InvenTree import InvenTree.helpers_model import InvenTree.version @@ -24,16 +23,12 @@ NotificationBody, trigger_notification, ) +from common.settings import get_global_setting from InvenTree.format import format_money logger = logging.getLogger('inventree') -def getSetting(key, backup_value=None): - """Shortcut for reading a setting value from the database.""" - return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value) - - def get_base_url(request=None): """Return the base URL for the InvenTree server. @@ -44,6 +39,8 @@ def get_base_url(request=None): 3. If settings.SITE_URL is set (e.g. in the Django settings), use that 4. If the InvenTree setting INVENTREE_BASE_URL is set, use that """ + import common.models + # Check if a request is provided if request: return request.build_absolute_uri('/') @@ -62,9 +59,7 @@ def get_base_url(request=None): # Check if a global InvenTree setting is provided try: - if site_url := common.models.InvenTreeSetting.get_setting( - 'INVENTREE_BASE_URL', create=False - ): + if site_url := get_global_setting('INVENTREE_BASE_URL', create=False): return site_url except (ProgrammingError, OperationalError): pass @@ -112,25 +107,20 @@ def download_image_from_url(remote_url, timeout=2.5): ValueError: Server responded with invalid 'Content-Length' value TypeError: Response is not a valid image """ + import common.models + # Check that the provided URL at least looks valid validator = URLValidator() validator(remote_url) # Calculate maximum allowable image size (in bytes) max_size = ( - int( - common.models.InvenTreeSetting.get_setting( - 'INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE' - ) - ) - * 1024 - * 1024 + int(get_global_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024 ) # Add user specified user-agent to request (if specified) - user_agent = common.models.InvenTreeSetting.get_setting( - 'INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT' - ) + user_agent = get_global_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT') + if user_agent: headers = {'User-Agent': user_agent} else: @@ -216,6 +206,8 @@ def render_currency( max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting. include_symbol: If True, include the currency symbol in the output """ + import common.models + if money in [None, '']: return '-' @@ -231,19 +223,13 @@ def render_currency( pass if decimal_places is None: - decimal_places = common.models.InvenTreeSetting.get_setting( - 'PRICING_DECIMAL_PLACES', 6 - ) + decimal_places = get_global_setting('PRICING_DECIMAL_PLACES', 6) if min_decimal_places is None: - min_decimal_places = common.models.InvenTreeSetting.get_setting( - 'PRICING_DECIMAL_PLACES_MIN', 0 - ) + min_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES_MIN', 0) if max_decimal_places is None: - max_decimal_places = common.models.InvenTreeSetting.get_setting( - 'PRICING_DECIMAL_PLACES', 6 - ) + max_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES', 6) value = Decimal(str(money.amount)).normalize() value = str(value) diff --git a/src/backend/InvenTree/InvenTree/social_auth_urls.py b/src/backend/InvenTree/InvenTree/social_auth_urls.py index 79f6bed08b0c..49d9e461ee40 100644 --- a/src/backend/InvenTree/InvenTree/social_auth_urls.py +++ b/src/backend/InvenTree/InvenTree/social_auth_urls.py @@ -15,7 +15,7 @@ from rest_framework.response import Response import InvenTree.sso -from common.models import InvenTreeSetting +from common.settings import get_global_setting from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer @@ -177,12 +177,10 @@ def get(self, request, *args, **kwargs): data = { 'sso_enabled': InvenTree.sso.login_enabled(), 'sso_registration': InvenTree.sso.registration_enabled(), - 'mfa_required': InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA'), + 'mfa_required': get_global_setting('LOGIN_ENFORCE_MFA'), 'providers': provider_list, - 'registration_enabled': InvenTreeSetting.get_setting('LOGIN_ENABLE_REG'), - 'password_forgotten_enabled': InvenTreeSetting.get_setting( - 'LOGIN_ENABLE_PWD_FORGOT' - ), + 'registration_enabled': get_global_setting('LOGIN_ENABLE_REG'), + 'password_forgotten_enabled': get_global_setting('LOGIN_ENABLE_PWD_FORGOT'), } return Response(data) diff --git a/src/backend/InvenTree/InvenTree/sso.py b/src/backend/InvenTree/InvenTree/sso.py index 0ad165a0b7d1..b3fb551cf2f1 100644 --- a/src/backend/InvenTree/InvenTree/sso.py +++ b/src/backend/InvenTree/InvenTree/sso.py @@ -2,7 +2,7 @@ import logging -from common.models import InvenTreeSetting +from common.settings import get_global_setting from InvenTree.helpers import str2bool logger = logging.getLogger('inventree') @@ -64,14 +64,14 @@ def provider_display_name(provider): def login_enabled() -> bool: """Return True if SSO login is enabled.""" - return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO')) + return str2bool(get_global_setting('LOGIN_ENABLE_SSO')) def registration_enabled() -> bool: """Return True if SSO registration is enabled.""" - return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG')) + return str2bool(get_global_setting('LOGIN_ENABLE_SSO_REG')) def auto_registration_enabled() -> bool: """Return True if SSO auto-registration is enabled.""" - return str2bool(InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO')) + return str2bool(get_global_setting('LOGIN_SIGNUP_SSO_AUTO')) diff --git a/src/backend/InvenTree/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py index 9b8366ef828c..65115d686212 100644 --- a/src/backend/InvenTree/InvenTree/tasks.py +++ b/src/backend/InvenTree/InvenTree/tasks.py @@ -26,6 +26,7 @@ set_maintenance_mode, ) +from common.settings import get_global_setting, set_global_setting from InvenTree.config import get_setting from plugin import registry @@ -90,7 +91,6 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool: Note that this function creates some *hidden* global settings (designated with the _ prefix), which are used to keep a running track of when the particular task was was last run. """ - from common.models import InvenTreeSetting from InvenTree.ready import isInTestMode if n_days <= 0: @@ -107,7 +107,7 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool: success_key = f'_{task_name}_SUCCESS' # Check for recent success information - last_success = InvenTreeSetting.get_setting(success_key, '', cache=False) + last_success = get_global_setting(success_key, '', cache=False) if last_success: try: @@ -125,7 +125,7 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool: return False # Check for any information we have about this task - last_attempt = InvenTreeSetting.get_setting(attempt_key, '', cache=False) + last_attempt = get_global_setting(attempt_key, '', cache=False) if last_attempt: try: @@ -152,22 +152,14 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool: def record_task_attempt(task_name: str): """Record that a multi-day task has been attempted *now*.""" - from common.models import InvenTreeSetting - logger.info("Logging task attempt for '%s'", task_name) - InvenTreeSetting.set_setting( - f'_{task_name}_ATTEMPT', datetime.now().isoformat(), None - ) + set_global_setting(f'_{task_name}_ATTEMPT', datetime.now().isoformat(), None) def record_task_success(task_name: str): """Record that a multi-day task was successful *now*.""" - from common.models import InvenTreeSetting - - InvenTreeSetting.set_setting( - f'_{task_name}_SUCCESS', datetime.now().isoformat(), None - ) + set_global_setting(f'_{task_name}_SUCCESS', datetime.now().isoformat(), None) def offload_task( @@ -380,9 +372,7 @@ def delete_successful_tasks(): try: from django_q.models import Success - from common.models import InvenTreeSetting - - days = InvenTreeSetting.get_setting('INVENTREE_DELETE_TASKS_DAYS', 30) + days = get_global_setting('INVENTREE_DELETE_TASKS_DAYS', 30) threshold = timezone.now() - timedelta(days=days) # Delete successful tasks @@ -404,9 +394,7 @@ def delete_failed_tasks(): try: from django_q.models import Failure - from common.models import InvenTreeSetting - - days = InvenTreeSetting.get_setting('INVENTREE_DELETE_TASKS_DAYS', 30) + days = get_global_setting('INVENTREE_DELETE_TASKS_DAYS', 30) threshold = timezone.now() - timedelta(days=days) # Delete failed tasks @@ -426,9 +414,7 @@ def delete_old_error_logs(): try: from error_report.models import Error - from common.models import InvenTreeSetting - - days = InvenTreeSetting.get_setting('INVENTREE_DELETE_ERRORS_DAYS', 30) + days = get_global_setting('INVENTREE_DELETE_ERRORS_DAYS', 30) threshold = timezone.now() - timedelta(days=days) errors = Error.objects.filter(when__lte=threshold) @@ -448,13 +434,9 @@ def delete_old_error_logs(): def delete_old_notifications(): """Delete old notification logs.""" try: - from common.models import ( - InvenTreeSetting, - NotificationEntry, - NotificationMessage, - ) + from common.models import NotificationEntry, NotificationMessage - days = InvenTreeSetting.get_setting('INVENTREE_DELETE_NOTIFICATIONS_DAYS', 30) + days = get_global_setting('INVENTREE_DELETE_NOTIFICATIONS_DAYS', 30) threshold = timezone.now() - timedelta(days=days) items = NotificationEntry.objects.filter(updated__lte=threshold) @@ -479,7 +461,6 @@ def delete_old_notifications(): def check_for_updates(): """Check if there is an update for InvenTree.""" try: - import common.models from common.notifications import trigger_superuser_notification except AppRegistryNotReady: # pragma: no cover # Apps not yet loaded! @@ -487,9 +468,7 @@ def check_for_updates(): return interval = int( - common.models.InvenTreeSetting.get_setting( - 'INVENTREE_UPDATE_CHECK_INTERVAL', 7, cache=False - ) + get_global_setting('INVENTREE_UPDATE_CHECK_INTERVAL', 7, cache=False) ) # Check if we should check for updates *today* @@ -538,7 +517,7 @@ def check_for_updates(): logger.info("Latest InvenTree version: '%s'", tag) # Save the version to the database - common.models.InvenTreeSetting.set_setting('_INVENTREE_LATEST_VERSION', tag, None) + set_global_setting('_INVENTREE_LATEST_VERSION', tag, None) # Record that this task was successful record_task_success('check_for_updates') @@ -572,7 +551,6 @@ def update_exchange_rates(force: bool = False): from djmoney.contrib.exchange.models import Rate from common.currency import currency_code_default, currency_codes - from common.models import InvenTreeSetting from InvenTree.exchange import InvenTreeExchange except AppRegistryNotReady: # pragma: no cover # Apps not yet loaded! @@ -585,9 +563,7 @@ def update_exchange_rates(force: bool = False): return if not force: - interval = int( - InvenTreeSetting.get_setting('CURRENCY_UPDATE_INTERVAL', 1, cache=False) - ) + interval = int(get_global_setting('CURRENCY_UPDATE_INTERVAL', 1, cache=False)) if not check_daily_holdoff('update_exchange_rates', interval): logger.info('Skipping exchange rate update (interval not reached)') @@ -617,15 +593,11 @@ def update_exchange_rates(force: bool = False): @scheduled_task(ScheduledTask.DAILY) def run_backup(): """Run the backup command.""" - from common.models import InvenTreeSetting - - if not InvenTreeSetting.get_setting('INVENTREE_BACKUP_ENABLE', False, cache=False): + if not get_global_setting('INVENTREE_BACKUP_ENABLE', False, cache=False): # Backups are not enabled - exit early return - interval = int( - InvenTreeSetting.get_setting('INVENTREE_BACKUP_DAYS', 1, cache=False) - ) + interval = int(get_global_setting('INVENTREE_BACKUP_DAYS', 1, cache=False)) # Check if should run this task *today* if not check_daily_holdoff('run_backup', interval): @@ -655,13 +627,12 @@ def check_for_migrations(force: bool = False, reload_registry: bool = True): If the setting auto_update is enabled we will start updating. """ - from common.models import InvenTreeSetting from plugin import registry def set_pending_migrations(n: int): """Helper function to inform the user about pending migrations.""" logger.info('There are %s pending migrations', n) - InvenTreeSetting.set_setting('_PENDING_MIGRATIONS', n, None) + set_global_setting('_PENDING_MIGRATIONS', n, None) logger.info('Checking for pending database migrations') diff --git a/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py b/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py index c6408d8f34ac..00641abc19e8 100644 --- a/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py +++ b/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py @@ -17,6 +17,7 @@ import InvenTree.helpers_model import plugin.models from common.currency import currency_code_default +from common.settings import get_global_setting from InvenTree import settings, version from plugin import registry from plugin.plugin import InvenTreePlugin @@ -135,7 +136,7 @@ def inventree_in_debug_mode(*args, **kwargs): @register.simple_tag() def inventree_show_about(user, *args, **kwargs): """Return True if the about modal should be shown.""" - if common.models.InvenTreeSetting.get_setting('INVENTREE_RESTRICT_ABOUT'): + if get_global_setting('INVENTREE_RESTRICT_ABOUT'): # Return False if the user is not a superuser, or no user information is provided if not user or not user.is_superuser: return False @@ -373,7 +374,7 @@ def settings_value(key, *args, **kwargs): return common.models.InvenTreeUserSetting.get_setting(key) return common.models.InvenTreeUserSetting.get_setting(key, user=kwargs['user']) - return common.models.InvenTreeSetting.get_setting(key) + return get_global_setting(key) @register.simple_tag() diff --git a/src/backend/InvenTree/InvenTree/test_tasks.py b/src/backend/InvenTree/InvenTree/test_tasks.py index eec5b39a6921..2cb1ae6f510e 100644 --- a/src/backend/InvenTree/InvenTree/test_tasks.py +++ b/src/backend/InvenTree/InvenTree/test_tasks.py @@ -122,6 +122,7 @@ def test_task_delete_old_error_logs(self): def test_task_check_for_updates(self): """Test the task check_for_updates.""" # Check that setting should be empty + InvenTreeSetting.set_setting('_INVENTREE_LATEST_VERSION', '') self.assertEqual(InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION'), '') # Get new version diff --git a/src/backend/InvenTree/InvenTree/version.py b/src/backend/InvenTree/InvenTree/version.py index eef574e1bd07..6c67752cb5ce 100644 --- a/src/backend/InvenTree/InvenTree/version.py +++ b/src/backend/InvenTree/InvenTree/version.py @@ -16,6 +16,8 @@ from dulwich.repo import NotGitRepository, Repo +from common.settings import get_global_setting + from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION # InvenTree software version @@ -51,17 +53,14 @@ def checkMinPythonVersion(): def inventreeInstanceName(): """Returns the InstanceName settings for the current database.""" - import common.models - - return common.models.InvenTreeSetting.get_setting('INVENTREE_INSTANCE', '') + return get_global_setting('INVENTREE_INSTANCE', '') def inventreeInstanceTitle(): """Returns the InstanceTitle for the current database.""" - import common.models + if get_global_setting('INVENTREE_INSTANCE_TITLE', False): + return get_global_setting('INVENTREE_INSTANCE', 'InvenTree') - if common.models.InvenTreeSetting.get_setting('INVENTREE_INSTANCE_TITLE', False): - return common.models.InvenTreeSetting.get_setting('INVENTREE_INSTANCE', '') return 'InvenTree' @@ -122,9 +121,7 @@ def isInvenTreeUpToDate(): A background task periodically queries GitHub for latest version, and stores it to the database as "_INVENTREE_LATEST_VERSION" """ - import common.models - - latest = common.models.InvenTreeSetting.get_setting( + latest = get_global_setting( '_INVENTREE_LATEST_VERSION', backup_value=None, create=False ) diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index ffbbc0b5441a..31bf282d59f1 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -36,6 +36,7 @@ import common.models from common.notifications import trigger_notification, InvenTreeNotificationBodies +from common.settings import get_global_setting from plugin.events import trigger_event import part.models @@ -136,7 +137,7 @@ def clean(self): super().clean() - if common.models.InvenTreeSetting.get_setting('BUILDORDER_REQUIRE_RESPONSIBLE'): + if get_global_setting('BUILDORDER_REQUIRE_RESPONSIBLE'): if not self.responsible: raise ValidationError({ 'responsible': _('Responsible user or group must be specified') diff --git a/src/backend/InvenTree/build/test_build.py b/src/backend/InvenTree/build/test_build.py index fd4fe9975f00..4f06038888bb 100644 --- a/src/backend/InvenTree/build/test_build.py +++ b/src/backend/InvenTree/build/test_build.py @@ -12,6 +12,7 @@ from InvenTree import status_codes as status import common.models +from common.settings import set_global_setting import build.tasks from build.models import Build, BuildItem, BuildLine, generate_next_build_reference from part.models import Part, BomItem, BomItemSubstitute, PartTestTemplate @@ -215,7 +216,7 @@ class BuildTest(BuildTestBase): def test_ref_int(self): """Test the "integer reference" field used for natural sorting""" # Set build reference to new value - common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref}-???', change_user=None) + set_global_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref}-???', change_user=None) refs = { 'BO-123-456': 123, @@ -238,7 +239,7 @@ def test_ref_int(self): self.assertEqual(build.reference_int, ref_int) # Set build reference back to default value - common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref:04d}', change_user=None) + set_global_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref:04d}', change_user=None) def test_ref_validation(self): """Test that the reference field validation works as expected""" @@ -271,7 +272,7 @@ def test_ref_validation(self): ) # Try a new validator pattern - common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', '{ref}-BO', change_user=None) + set_global_setting('BUILDORDER_REFERENCE_PATTERN', '{ref}-BO', change_user=None) for ref in [ '1234-BO', @@ -285,11 +286,11 @@ def test_ref_validation(self): ) # Set build reference back to default value - common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref:04d}', change_user=None) + set_global_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref:04d}', change_user=None) def test_next_ref(self): """Test that the next reference is automatically generated""" - common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'XYZ-{ref:06d}', change_user=None) + set_global_setting('BUILDORDER_REFERENCE_PATTERN', 'XYZ-{ref:06d}', change_user=None) build = Build.objects.create( part=self.assembly, @@ -311,7 +312,7 @@ def test_next_ref(self): self.assertEqual(build.reference_int, 988) # Set build reference back to default value - common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref:04d}', change_user=None) + set_global_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref:04d}', change_user=None) def test_init(self): """Perform some basic tests before we start the ball rolling""" @@ -647,7 +648,7 @@ def test_complete_with_required_tests(self): """Test the prevention completion when a required test is missing feature""" # with required tests incompleted the save should fail - common.models.InvenTreeSetting.set_setting('PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS', True, change_user=None) + set_global_setting('PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS', True, change_user=None) with self.assertRaises(ValidationError): self.build_w_tests_trackable.complete_build_output(self.stockitem_with_required_test, None) diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index 3d1cda90f6f3..6965b21ec8cc 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -22,6 +22,7 @@ import common.models import common.serializers +from common.settings import get_global_setting from generic.states.api import AllStatusViews, StatusView from InvenTree.api import BulkDeleteMixin, MetadataView from InvenTree.config import CONFIG_LOOKUPS @@ -149,7 +150,7 @@ def get(self, request, format=None): updated = None response = { - 'base_currency': common.models.InvenTreeSetting.get_setting( + 'base_currency': get_global_setting( 'INVENTREE_DEFAULT_CURRENCY', backup_value='USD' ), 'exchange_rates': {}, diff --git a/src/backend/InvenTree/common/apps.py b/src/backend/InvenTree/common/apps.py index a62c15d59ffa..65364992293b 100644 --- a/src/backend/InvenTree/common/apps.py +++ b/src/backend/InvenTree/common/apps.py @@ -5,6 +5,7 @@ from django.apps import AppConfig import InvenTree.ready +from common.settings import get_global_setting, set_global_setting logger = logging.getLogger('inventree') @@ -27,16 +28,12 @@ def ready(self): def clear_restart_flag(self): """Clear the SERVER_RESTART_REQUIRED setting.""" try: - import common.models - - if common.models.InvenTreeSetting.get_setting( + if get_global_setting( 'SERVER_RESTART_REQUIRED', backup_value=False, create=False, cache=False ): logger.info('Clearing SERVER_RESTART_REQUIRED flag') if not InvenTree.ready.isImportingData(): - common.models.InvenTreeSetting.set_setting( - 'SERVER_RESTART_REQUIRED', False, None - ) + set_global_setting('SERVER_RESTART_REQUIRED', False, None) except Exception: pass diff --git a/src/backend/InvenTree/common/currency.py b/src/backend/InvenTree/common/currency.py index c77549c5509b..4c0c887b5ebd 100644 --- a/src/backend/InvenTree/common/currency.py +++ b/src/backend/InvenTree/common/currency.py @@ -17,7 +17,7 @@ def currency_code_default(): """Returns the default currency code (or USD if not specified).""" - from common.models import InvenTreeSetting + from common.settings import get_global_setting try: cached_value = cache.get('currency_code_default', '') @@ -28,7 +28,7 @@ def currency_code_default(): return cached_value try: - code = InvenTreeSetting.get_setting( + code = get_global_setting( 'INVENTREE_DEFAULT_CURRENCY', backup_value='', create=True, cache=True ) except Exception: # pragma: no cover @@ -59,9 +59,9 @@ def currency_codes_default_list() -> str: def currency_codes() -> list: """Returns the current currency codes.""" - from common.models import InvenTreeSetting + from common.settings import get_global_setting - codes = InvenTreeSetting.get_setting('CURRENCY_CODES', '', create=False).strip() + codes = get_global_setting('CURRENCY_CODES', '', create=False).strip() if not codes: codes = currency_codes_default_list() @@ -150,6 +150,9 @@ def currency_exchange_plugins() -> list: except Exception: plugs = [] + if len(plugs) == 0: + return None + return [('', _('No plugin'))] + [(plug.slug, plug.human_name) for plug in plugs] diff --git a/src/backend/InvenTree/common/settings.py b/src/backend/InvenTree/common/settings.py index 67a96c3510ee..d27ddfa2f4c7 100644 --- a/src/backend/InvenTree/common/settings.py +++ b/src/backend/InvenTree/common/settings.py @@ -1,6 +1,44 @@ """User-configurable settings for the common app.""" +def get_global_setting(key, backup_value=None, **kwargs): + """Return the value of a global setting using the provided key.""" + from common.models import InvenTreeSetting + + kwargs['backup_value'] = backup_value + + return InvenTreeSetting.get_setting(key, **kwargs) + + +def set_global_setting(key, value, change_user=None, create=True, **kwargs): + """Set the value of a global setting using the provided key.""" + from common.models import InvenTreeSetting + + kwargs['change_user'] = change_user + kwargs['create'] = create + + return InvenTreeSetting.set_setting(key, value, **kwargs) + + +def get_user_setting(key, user, backup_value=None, **kwargs): + """Return the value of a user-specific setting using the provided key.""" + from common.models import InvenTreeUserSetting + + kwargs['user'] = user + kwargs['backup_value'] = backup_value + + return InvenTreeUserSetting.get_setting(key, **kwargs) + + +def set_user_setting(key, value, user, **kwargs): + """Set the value of a user-specific setting using the provided key.""" + from common.models import InvenTreeUserSetting + + kwargs['user'] = user + + return InvenTreeUserSetting.set_setting(key, value, **kwargs) + + def stock_expiry_enabled(): """Returns True if the stock expiry feature is enabled.""" from common.models import InvenTreeSetting diff --git a/src/backend/InvenTree/common/tests.py b/src/backend/InvenTree/common/tests.py index 7aad13713ccd..fda28be67e33 100644 --- a/src/backend/InvenTree/common/tests.py +++ b/src/backend/InvenTree/common/tests.py @@ -18,6 +18,7 @@ import PIL +from common.settings import get_global_setting, set_global_setting from InvenTree.helpers import str2bool from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase, PluginMixin from plugin import registry @@ -273,13 +274,19 @@ def test_setting_data(self): print(f"run_settings_check failed for user setting '{key}'") raise exc - @override_settings(SITE_URL=None) + @override_settings(SITE_URL=None, PLUGIN_TESTING=True, PLUGIN_TESTING_SETUP=True) def test_defaults(self): """Populate the settings with default values.""" + N = len(InvenTreeSetting.SETTINGS.keys()) + for key in InvenTreeSetting.SETTINGS.keys(): value = InvenTreeSetting.get_setting_default(key) - InvenTreeSetting.set_setting(key, value, self.user) + try: + InvenTreeSetting.set_setting(key, value, change_user=self.user) + except Exception as exc: + print(f"test_defaults: Failed to set default value for setting '{key}'") + raise exc self.assertEqual(value, InvenTreeSetting.get_setting(key)) @@ -287,11 +294,6 @@ def test_defaults(self): setting = InvenTreeSetting.get_setting_object(key) if setting.is_bool(): - if setting.default_value in ['', None]: - raise ValueError( - f'Default value for boolean setting {key} not provided' - ) # pragma: no cover - if setting.default_value not in [True, False]: raise ValueError( f'Non-boolean default value specified for {key}' @@ -975,17 +977,13 @@ def test_restart_flag(self): from plugin import registry # set flag true - common.models.InvenTreeSetting.set_setting( - 'SERVER_RESTART_REQUIRED', True, None - ) + set_global_setting('SERVER_RESTART_REQUIRED', True, None) # reload the app registry.reload_plugins() # now it should be false again - self.assertFalse( - common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED') - ) + self.assertFalse(get_global_setting('SERVER_RESTART_REQUIRED')) def test_config_api(self): """Test config URLs.""" diff --git a/src/backend/InvenTree/common/validators.py b/src/backend/InvenTree/common/validators.py index 36c66c7f1237..d97983f5a342 100644 --- a/src/backend/InvenTree/common/validators.py +++ b/src/backend/InvenTree/common/validators.py @@ -5,7 +5,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -import InvenTree.helpers_model +from common.settings import get_global_setting def validate_notes_model_type(value): @@ -13,6 +13,7 @@ def validate_notes_model_type(value): The provided value must map to a model which implements the 'InvenTreeNotesMixin'. """ + import InvenTree.helpers_model import InvenTree.models if not value: @@ -31,11 +32,9 @@ def validate_notes_model_type(value): def validate_decimal_places_min(value): """Validator for PRICING_DECIMAL_PLACES_MIN setting.""" - from common.models import InvenTreeSetting - try: value = int(value) - places_max = int(InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES')) + places_max = int(get_global_setting('PRICING_DECIMAL_PLACES', create=False)) except Exception: return @@ -45,11 +44,9 @@ def validate_decimal_places_min(value): def validate_decimal_places_max(value): """Validator for PRICING_DECIMAL_PLACES_MAX setting.""" - from common.models import InvenTreeSetting - try: value = int(value) - places_min = int(InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES_MIN')) + places_min = int(get_global_setting('PRICING_DECIMAL_PLACES_MIN', create=False)) except Exception: return diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 36dad6aa9f4a..8fb2c8449cb0 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -35,6 +35,7 @@ import users.models as UserModels from common.currency import currency_code_default from common.notifications import InvenTreeNotificationBodies +from common.settings import get_global_setting from company.models import Address, Company, Contact, SupplierPart from generic.states import StateTransitionMixin from InvenTree.exceptions import log_error @@ -44,7 +45,7 @@ RoundingDecimalField, ) from InvenTree.helpers import decimal2string, pui_url -from InvenTree.helpers_model import getSetting, notify_responsible +from InvenTree.helpers_model import notify_responsible from order.status_codes import ( PurchaseOrderStatus, PurchaseOrderStatusGroups, @@ -232,9 +233,7 @@ def clean(self): # Check if a responsible owner is required for this order type if self.REQUIRE_RESPONSIBLE_SETTING: - if common_models.InvenTreeSetting.get_setting( - self.REQUIRE_RESPONSIBLE_SETTING, backup_value=False - ): + if get_global_setting(self.REQUIRE_RESPONSIBLE_SETTING, backup_value=False): if not self.responsible: raise ValidationError({ 'responsible': _('Responsible user or group must be specified') @@ -820,9 +819,7 @@ def receive_line_item( # Has this order been completed? if len(self.pending_line_items()) == 0: - if common_models.InvenTreeSetting.get_setting( - 'PURCHASEORDER_AUTO_COMPLETE', True - ): + if get_global_setting('PURCHASEORDER_AUTO_COMPLETE', True): self.received_by = user self.complete_order() # This will save the model @@ -1073,7 +1070,7 @@ def _action_complete(self, *args, **kwargs): return False bypass_shipped = InvenTree.helpers.str2bool( - common_models.InvenTreeSetting.get_setting('SALESORDER_SHIP_COMPLETE') + get_global_setting('SALESORDER_SHIP_COMPLETE') ) if bypass_shipped or self.status == SalesOrderStatus.SHIPPED: @@ -1231,7 +1228,7 @@ def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs if created: # A new SalesOrder has just been created - if getSetting('SALESORDER_DEFAULT_SHIPMENT'): + if get_global_setting('SALESORDER_DEFAULT_SHIPMENT'): # Create default shipment SalesOrderShipment.objects.create(order=instance, reference='1') diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 5626d8d0601b..bd84892ff3f3 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -50,6 +50,7 @@ from build.status_codes import BuildStatusGroups from common.currency import currency_code_default from common.models import InvenTreeSetting +from common.settings import get_global_setting, set_global_setting from company.models import SupplierPart from InvenTree import helpers, validators from InvenTree.fields import InvenTreeURLField @@ -482,9 +483,7 @@ def delete(self, **kwargs): if self.active: raise ValidationError(_('Cannot delete this part as it is still active')) - if not common.models.InvenTreeSetting.get_setting( - 'PART_ALLOW_DELETE_FROM_ASSEMBLY', cache=False - ): + if not get_global_setting('PART_ALLOW_DELETE_FROM_ASSEMBLY', cache=False): if BomItem.objects.filter(sub_part=self).exists(): raise ValidationError( _('Cannot delete this part as it is used in an assembly') @@ -649,9 +648,7 @@ def validate_ipn(self, raise_error=True): raise ValidationError({'IPN': exc.message}) # If we get to here, none of the plugins have raised an error - pattern = common.models.InvenTreeSetting.get_setting( - 'PART_IPN_REGEX', '', create=False - ).strip() + pattern = get_global_setting('PART_IPN_REGEX', '', create=False).strip() if pattern: match = re.search(pattern, self.IPN) @@ -719,9 +716,7 @@ def validate_serial_number( from part.models import Part from stock.models import StockItem - if common.models.InvenTreeSetting.get_setting( - 'SERIAL_NUMBER_GLOBALLY_UNIQUE', False - ): + if get_global_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False): # Serial number must be unique across *all* parts parts = Part.objects.all() else: @@ -775,9 +770,7 @@ def get_latest_serial_number(self): ) # Generate a query for any stock items for this part variant tree with non-empty serial numbers - if common.models.InvenTreeSetting.get_setting( - 'SERIAL_NUMBER_GLOBALLY_UNIQUE', False - ): + if get_global_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False): # Serial numbers are unique across all parts pass else: @@ -831,9 +824,7 @@ def validate_unique(self, exclude=None): super().validate_unique(exclude) # User can decide whether duplicate IPN (Internal Part Number) values are allowed - allow_duplicate_ipn = common.models.InvenTreeSetting.get_setting( - 'PART_ALLOW_DUPLICATE_IPN' - ) + allow_duplicate_ipn = get_global_setting('PART_ALLOW_DUPLICATE_IPN') # Raise an error if an IPN is set, and it is a duplicate if self.IPN and not allow_duplicate_ipn: @@ -2749,11 +2740,11 @@ def update_purchase_cost(self, save=True): purchase_max = purchase_cost # Also check if manual stock item pricing is included - if InvenTreeSetting.get_setting('PRICING_USE_STOCK_PRICING', True): + if get_global_setting('PRICING_USE_STOCK_PRICING', True): items = self.part.stock_items.all() # Limit to stock items updated within a certain window - days = int(InvenTreeSetting.get_setting('PRICING_STOCK_ITEM_AGE_DAYS', 0)) + days = int(get_global_setting('PRICING_STOCK_ITEM_AGE_DAYS', 0)) if days > 0: date_threshold = InvenTree.helpers.current_date() - timedelta(days=days) @@ -2789,7 +2780,7 @@ def update_internal_cost(self, save=True): min_int_cost = None max_int_cost = None - if InvenTreeSetting.get_setting('PART_INTERNAL_PRICE', False): + if get_global_setting('PART_INTERNAL_PRICE', False): # Only calculate internal pricing if internal pricing is enabled for pb in self.part.internalpricebreaks.all(): cost = self.convert(pb.price) @@ -2865,7 +2856,7 @@ def update_variant_cost(self, save=True): variant_min = None variant_max = None - active_only = InvenTreeSetting.get_setting('PRICING_ACTIVE_VARIANTS', False) + active_only = get_global_setting('PRICING_ACTIVE_VARIANTS', False) if self.part.is_template: variants = self.part.get_descendants(include_self=False) @@ -2907,11 +2898,11 @@ def update_overall_cost(self): max_costs = [self.bom_cost_max, self.purchase_cost_max, self.internal_cost_max] - purchase_history_override = InvenTreeSetting.get_setting( + purchase_history_override = get_global_setting( 'PRICING_PURCHASE_HISTORY_OVERRIDES_SUPPLIER', False ) - if InvenTreeSetting.get_setting('PRICING_USE_SUPPLIER_PRICING', True): + if get_global_setting('PRICING_USE_SUPPLIER_PRICING', True): # Add supplier pricing data, *unless* historical pricing information should override if self.purchase_cost_min is None or not purchase_history_override: min_costs.append(self.supplier_price_min) @@ -2919,7 +2910,7 @@ def update_overall_cost(self): if self.purchase_cost_max is None or not purchase_history_override: max_costs.append(self.supplier_price_max) - if InvenTreeSetting.get_setting('PRICING_USE_VARIANT_PRICING', True): + if get_global_setting('PRICING_USE_VARIANT_PRICING', True): # Include variant pricing in overall calculations min_costs.append(self.variant_cost_min) max_costs.append(self.variant_cost_max) @@ -2946,7 +2937,7 @@ def update_overall_cost(self): if overall_max is None or cost > overall_max: overall_max = cost - if InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False): + if get_global_setting('PART_BOM_USE_INTERNAL_PRICE', False): # Check if internal pricing should override other pricing if self.internal_cost_min is not None: overall_min = self.internal_cost_min @@ -3774,7 +3765,7 @@ def clean(self): super().clean() # Validate the parameter data against the template units - if InvenTreeSetting.get_setting( + if get_global_setting( 'PART_PARAMETER_ENFORCE_UNITS', True, cache=False, create=False ): if self.template.units: @@ -3916,7 +3907,7 @@ def clean(self): if ( self.default_value - and InvenTreeSetting.get_setting( + and get_global_setting( 'PART_PARAMETER_ENFORCE_UNITS', True, cache=False, create=False ) and self.parameter_template.units @@ -4325,9 +4316,7 @@ def get_required_quantity(self, build_quantity): def price_range(self, internal=False): """Return the price-range for this BOM item.""" # get internal price setting - use_internal = common.models.InvenTreeSetting.get_setting( - 'PART_BOM_USE_INTERNAL_PRICE', False - ) + use_internal = get_global_setting('PART_BOM_USE_INTERNAL_PRICE', False) prange = self.sub_part.get_price_range( self.quantity, internal=use_internal and internal ) diff --git a/src/backend/InvenTree/part/settings.py b/src/backend/InvenTree/part/settings.py index e5706a87ffc1..f258fbcb2078 100644 --- a/src/backend/InvenTree/part/settings.py +++ b/src/backend/InvenTree/part/settings.py @@ -1,38 +1,38 @@ """User-configurable settings for the Part app.""" -from common.models import InvenTreeSetting +from common.settings import get_global_setting def part_assembly_default(): """Returns the default value for the 'assembly' field of a Part object.""" - return InvenTreeSetting.get_setting('PART_ASSEMBLY') + return get_global_setting('PART_ASSEMBLY') def part_template_default(): """Returns the default value for the 'is_template' field of a Part object.""" - return InvenTreeSetting.get_setting('PART_TEMPLATE') + return get_global_setting('PART_TEMPLATE') def part_virtual_default(): """Returns the default value for the 'is_virtual' field of Part object.""" - return InvenTreeSetting.get_setting('PART_VIRTUAL') + return get_global_setting('PART_VIRTUAL') def part_component_default(): """Returns the default value for the 'component' field of a Part object.""" - return InvenTreeSetting.get_setting('PART_COMPONENT') + return get_global_setting('PART_COMPONENT') def part_purchaseable_default(): """Returns the default value for the 'purchasable' field for a Part object.""" - return InvenTreeSetting.get_setting('PART_PURCHASEABLE') + return get_global_setting('PART_PURCHASEABLE') def part_salable_default(): """Returns the default value for the 'salable' field for a Part object.""" - return InvenTreeSetting.get_setting('PART_SALABLE') + return get_global_setting('PART_SALABLE') def part_trackable_default(): """Returns the default value for the 'trackable' field for a Part object.""" - return InvenTreeSetting.get_setting('PART_TRACKABLE') + return get_global_setting('PART_TRACKABLE') diff --git a/src/backend/InvenTree/part/tasks.py b/src/backend/InvenTree/part/tasks.py index 768be1a440fa..ec30fdfdb0f4 100644 --- a/src/backend/InvenTree/part/tasks.py +++ b/src/backend/InvenTree/part/tasks.py @@ -9,15 +9,14 @@ from django.utils.translation import gettext_lazy as _ import common.currency -import common.models import common.notifications -import common.settings import company.models import InvenTree.helpers import InvenTree.helpers_model import InvenTree.tasks import part.models import part.stocktake +from common.settings import get_global_setting from InvenTree.tasks import ( ScheduledTask, check_daily_holdoff, @@ -99,7 +98,7 @@ def check_missing_pricing(limit=250): pp.schedule_for_update() # Find any parts which have 'old' pricing information - days = int(common.models.InvenTreeSetting.get_setting('PRICING_UPDATE_DAYS', 30)) + days = int(get_global_setting('PRICING_UPDATE_DAYS', 30)) stale_date = datetime.now().date() - timedelta(days=days) results = part.models.PartPricing.objects.filter(updated__lte=stale_date)[:limit] @@ -146,9 +145,7 @@ def scheduled_stocktake_reports(): # First let's delete any old stocktake reports delete_n_days = int( - common.models.InvenTreeSetting.get_setting( - 'STOCKTAKE_DELETE_REPORT_DAYS', 30, cache=False - ) + get_global_setting('STOCKTAKE_DELETE_REPORT_DAYS', 30, cache=False) ) threshold = datetime.now() - timedelta(days=delete_n_days) old_reports = part.models.PartStocktakeReport.objects.filter(date__lt=threshold) @@ -158,17 +155,11 @@ def scheduled_stocktake_reports(): old_reports.delete() # Next, check if stocktake functionality is enabled - if not common.models.InvenTreeSetting.get_setting( - 'STOCKTAKE_ENABLE', False, cache=False - ): + if not get_global_setting('STOCKTAKE_ENABLE', False, cache=False): logger.info('Stocktake functionality is not enabled - exiting') return - report_n_days = int( - common.models.InvenTreeSetting.get_setting( - 'STOCKTAKE_AUTO_DAYS', 0, cache=False - ) - ) + report_n_days = int(get_global_setting('STOCKTAKE_AUTO_DAYS', 0, cache=False)) if report_n_days < 1: logger.info('Stocktake auto reports are disabled, exiting') diff --git a/src/backend/InvenTree/part/test_part.py b/src/backend/InvenTree/part/test_part.py index 9c70ea23caf1..58a88c3055b8 100644 --- a/src/backend/InvenTree/part/test_part.py +++ b/src/backend/InvenTree/part/test_part.py @@ -18,6 +18,7 @@ NotificationMessage, ) from common.notifications import UIMessageNotification, storage +from common.settings import get_global_setting, set_global_setting from InvenTree import version from InvenTree.templatetags import inventree_extras from InvenTree.unit_test import InvenTreeTestCase @@ -500,17 +501,17 @@ def test_initial(self): def test_custom(self): """Update some of the part values and re-test.""" for val in [True, False]: - InvenTreeSetting.set_setting('PART_COMPONENT', val, self.user) - InvenTreeSetting.set_setting('PART_PURCHASEABLE', val, self.user) - InvenTreeSetting.set_setting('PART_SALABLE', val, self.user) - InvenTreeSetting.set_setting('PART_TRACKABLE', val, self.user) - InvenTreeSetting.set_setting('PART_ASSEMBLY', val, self.user) - InvenTreeSetting.set_setting('PART_TEMPLATE', val, self.user) - - self.assertEqual(val, InvenTreeSetting.get_setting('PART_COMPONENT')) - self.assertEqual(val, InvenTreeSetting.get_setting('PART_PURCHASEABLE')) - self.assertEqual(val, InvenTreeSetting.get_setting('PART_SALABLE')) - self.assertEqual(val, InvenTreeSetting.get_setting('PART_TRACKABLE')) + set_global_setting('PART_COMPONENT', val, self.user) + set_global_setting('PART_PURCHASEABLE', val, self.user) + set_global_setting('PART_SALABLE', val, self.user) + set_global_setting('PART_TRACKABLE', val, self.user) + set_global_setting('PART_ASSEMBLY', val, self.user) + set_global_setting('PART_TEMPLATE', val, self.user) + + self.assertEqual(val, get_global_setting('PART_COMPONENT')) + self.assertEqual(val, get_global_setting('PART_PURCHASEABLE')) + self.assertEqual(val, get_global_setting('PART_SALABLE')) + self.assertEqual(val, get_global_setting('PART_TRACKABLE')) part = self.make_part() @@ -546,7 +547,7 @@ def test_duplicate_ipn(self): part.validate_unique() # Now update the settings so duplicate IPN values are *not* allowed - InvenTreeSetting.set_setting('PART_ALLOW_DUPLICATE_IPN', False, self.user) + set_global_setting('PART_ALLOW_DUPLICATE_IPN', False, self.user) with self.assertRaises(ValidationError): part = Part(name='Hello', description='A thing', IPN='IPN123', revision='C') diff --git a/src/backend/InvenTree/part/test_pricing.py b/src/backend/InvenTree/part/test_pricing.py index 2cdc4c54a632..bf1b6764cc56 100644 --- a/src/backend/InvenTree/part/test_pricing.py +++ b/src/backend/InvenTree/part/test_pricing.py @@ -12,6 +12,7 @@ import order.models import part.models import stock.models +from common.settings import get_global_setting, set_global_setting from InvenTree.unit_test import InvenTreeTestCase from order.status_codes import PurchaseOrderStatus @@ -172,7 +173,7 @@ def test_supplier_part_pricing(self): def test_internal_pricing(self): """Tests for internal price breaks.""" # Ensure internal pricing is enabled - common.models.InvenTreeSetting.set_setting('PART_INTERNAL_PRICE', True, None) + set_global_setting('PART_INTERNAL_PRICE', True, None) pricing = self.part.pricing @@ -221,9 +222,7 @@ def test_stock_item_pricing(self): ) # Ensure that initially, stock item pricing is disabled - common.models.InvenTreeSetting.set_setting( - 'PRICING_USE_STOCK_PRICING', False, None - ) + set_global_setting('PRICING_USE_STOCK_PRICING', False, None) pricing = p.pricing pricing.update_pricing() @@ -235,9 +234,7 @@ def test_stock_item_pricing(self): self.assertIsNone(pricing.overall_max) # Turn on stock pricing - common.models.InvenTreeSetting.set_setting( - 'PRICING_USE_STOCK_PRICING', True, None - ) + set_global_setting('PRICING_USE_STOCK_PRICING', True, None) pricing.update_pricing() diff --git a/src/backend/InvenTree/plugin/base/event/events.py b/src/backend/InvenTree/plugin/base/event/events.py index 33a43b75f9f3..ee5a81476324 100644 --- a/src/backend/InvenTree/plugin/base/event/events.py +++ b/src/backend/InvenTree/plugin/base/event/events.py @@ -8,6 +8,7 @@ from django.dispatch.dispatcher import receiver import InvenTree.exceptions +from common.settings import get_global_setting from InvenTree.ready import canAppAccessDatabase, isImportingData from InvenTree.tasks import offload_task from plugin.registry import registry @@ -21,9 +22,7 @@ def trigger_event(event, *args, **kwargs): This event will be stored in the database, and the worker will respond to it later on. """ - from common.models import InvenTreeSetting - - if not InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS', False): + if not get_global_setting('ENABLE_PLUGINS_EVENTS', False): # Do nothing if plugin events are not enabled return @@ -50,12 +49,10 @@ def register_event(event, *args, **kwargs): Note: This function is processed by the background worker, as it performs multiple database access operations. """ - from common.models import InvenTreeSetting - logger.debug("Registering triggered event: '%s'", event) # Determine if there are any plugins which are interested in responding - if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'): + if settings.PLUGIN_TESTING or get_global_setting('ENABLE_PLUGINS_EVENTS'): # Check if the plugin registry needs to be reloaded registry.check_reload() diff --git a/src/backend/InvenTree/plugin/base/integration/AppMixin.py b/src/backend/InvenTree/plugin/base/integration/AppMixin.py index 196b941253fb..6c102cf9a8e5 100644 --- a/src/backend/InvenTree/plugin/base/integration/AppMixin.py +++ b/src/backend/InvenTree/plugin/base/integration/AppMixin.py @@ -38,11 +38,9 @@ def _activate_mixin( force_reload (bool, optional): Only reload base apps. Defaults to False. full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False. """ - from common.models import InvenTreeSetting + from common.settings import get_global_setting - if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting( - 'ENABLE_PLUGINS_APP' - ): + if settings.PLUGIN_TESTING or get_global_setting('ENABLE_PLUGINS_APP'): logger.info('Registering IntegrationPlugin apps') apps_changed = False diff --git a/src/backend/InvenTree/plugin/base/integration/UrlsMixin.py b/src/backend/InvenTree/plugin/base/integration/UrlsMixin.py index 7a5972c0d8f6..aa526b2dab04 100644 --- a/src/backend/InvenTree/plugin/base/integration/UrlsMixin.py +++ b/src/backend/InvenTree/plugin/base/integration/UrlsMixin.py @@ -5,6 +5,7 @@ from django.conf import settings from django.urls import include, re_path +from common.settings import get_global_setting from plugin.urls import PLUGIN_BASE logger = logging.getLogger('inventree') @@ -36,11 +37,7 @@ def _activate_mixin( force_reload (bool, optional): Only reload base apps. Defaults to False. full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False. """ - from common.models import InvenTreeSetting - - if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting( - 'ENABLE_PLUGINS_URL' - ): + if settings.PLUGIN_TESTING or get_global_setting('ENABLE_PLUGINS_URL'): logger.info('Registering UrlsMixin Plugin') urls_changed = False # check whether an activated plugin extends UrlsMixin diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 27b307df5469..f837c64ce0a3 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -25,6 +25,7 @@ from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ +from common.settings import get_global_setting, set_global_setting from InvenTree.config import get_plugin_dir from InvenTree.ready import canAppAccessDatabase @@ -732,12 +733,10 @@ def _update_urls(self): # region plugin registry hash calculations def update_plugin_hash(self): """When the state of the plugin registry changes, update the hash.""" - from common.models import InvenTreeSetting - self.registry_hash = self.calculate_plugin_hash() try: - old_hash = InvenTreeSetting.get_setting( + old_hash = get_global_setting( '_PLUGIN_REGISTRY_HASH', '', create=False, cache=False ) except Exception: @@ -748,7 +747,7 @@ def update_plugin_hash(self): logger.debug( 'Updating plugin registry hash: %s', str(self.registry_hash) ) - InvenTreeSetting.set_setting( + set_global_setting( '_PLUGIN_REGISTRY_HASH', self.registry_hash, change_user=None ) except (OperationalError, ProgrammingError): @@ -776,8 +775,6 @@ def calculate_plugin_hash(self): """ from hashlib import md5 - from common.models import InvenTreeSetting - data = md5() # Hash for all loaded plugins @@ -789,7 +786,7 @@ def calculate_plugin_hash(self): for k in self.plugin_settings_keys(): try: - val = InvenTreeSetting.get_setting(k, False, create=False) + val = get_global_setting(k, False, create=False) msg = f'{k}-{val}' data.update(msg.encode()) @@ -800,8 +797,6 @@ def calculate_plugin_hash(self): def check_reload(self): """Determine if the registry needs to be reloaded.""" - from common.models import InvenTreeSetting - if settings.TESTING: # Skip if running during unit testing return @@ -817,9 +812,7 @@ def check_reload(self): self.registry_hash = self.calculate_plugin_hash() try: - reg_hash = InvenTreeSetting.get_setting( - '_PLUGIN_REGISTRY_HASH', '', create=False - ) + reg_hash = get_global_setting('_PLUGIN_REGISTRY_HASH', '', create=False) except Exception as exc: logger.exception('Failed to retrieve plugin registry hash: %s', str(exc)) return diff --git a/src/backend/InvenTree/plugin/templatetags/plugin_extras.py b/src/backend/InvenTree/plugin/templatetags/plugin_extras.py index 26612b76badd..38abaa650e44 100644 --- a/src/backend/InvenTree/plugin/templatetags/plugin_extras.py +++ b/src/backend/InvenTree/plugin/templatetags/plugin_extras.py @@ -4,8 +4,8 @@ from django.conf import settings as djangosettings from django.urls import reverse -from common.models import InvenTreeSetting from common.notifications import storage +from common.settings import get_global_setting from plugin.registry import registry register = template.Library() @@ -55,7 +55,7 @@ def navigation_enabled(*args, **kwargs): """Is plugin navigation enabled?""" if djangosettings.PLUGIN_TESTING: return True - return InvenTreeSetting.get_setting('ENABLE_PLUGINS_NAVIGATION') # pragma: no cover + return get_global_setting('ENABLE_PLUGINS_NAVIGATION') # pragma: no cover @register.simple_tag() diff --git a/src/backend/InvenTree/plugin/test_api.py b/src/backend/InvenTree/plugin/test_api.py index ed4ae7439b00..bdef4481e367 100644 --- a/src/backend/InvenTree/plugin/test_api.py +++ b/src/backend/InvenTree/plugin/test_api.py @@ -35,7 +35,7 @@ def test_plugin_install(self): 'packagename': 'invalid_package_name-asdads-asfd-asdf-asdf-asdf', }, expected_code=400, - max_query_time=20, + max_query_time=30, ) # valid - Pypi @@ -43,7 +43,7 @@ def test_plugin_install(self): url, {'confirm': True, 'packagename': self.PKG_NAME}, expected_code=201, - max_query_time=20, + max_query_time=30, ).data self.assertEqual(data['success'], 'Installed plugin successfully') @@ -53,7 +53,7 @@ def test_plugin_install(self): url, {'confirm': True, 'url': self.PKG_URL}, expected_code=201, - max_query_time=20, + max_query_time=30, ).data self.assertEqual(data['success'], 'Installed plugin successfully') @@ -63,7 +63,7 @@ def test_plugin_install(self): url, {'confirm': True, 'url': self.PKG_URL, 'packagename': self.PKG_NAME}, expected_code=201, - max_query_time=20, + max_query_time=30, ).data self.assertEqual(data['success'], 'Installed plugin successfully') diff --git a/src/backend/InvenTree/report/helpers.py b/src/backend/InvenTree/report/helpers.py index c0720ce348d9..a790b02662e4 100644 --- a/src/backend/InvenTree/report/helpers.py +++ b/src/backend/InvenTree/report/helpers.py @@ -6,6 +6,8 @@ from django.utils.translation import gettext_lazy as _ +from common.settings import get_global_setting + logger = logging.getLogger('inventree') @@ -67,10 +69,8 @@ def page_size(page_code): def report_page_size_default(): """Returns the default page size for PDF reports.""" - from common.models import InvenTreeSetting - try: - page_size = InvenTreeSetting.get_setting('REPORT_DEFAULT_PAGE_SIZE', 'A4') + page_size = get_global_setting('REPORT_DEFAULT_PAGE_SIZE', 'A4') except Exception as exc: logger.exception('Error getting default page size: %s', str(exc)) page_size = 'A4' diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index aea2105abbf8..92fe1408c268 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -15,7 +15,7 @@ import InvenTree.helpers import InvenTree.helpers_model import report.helpers -from common.models import InvenTreeSetting +from common.settings import get_global_setting from company.models import Company from part.models import Part @@ -87,7 +87,7 @@ def asset(filename): filename = '' + filename # If in debug mode, return URL to the image, not a local file - debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE', cache=False) + debug_mode = get_global_setting('REPORT_DEBUG_MODE', cache=False) # Test if the file actually exists full_path = settings.MEDIA_ROOT.joinpath('report', 'assets', filename).resolve() @@ -132,7 +132,7 @@ def uploaded_image( filename = '' + filename # If in debug mode, return URL to the image, not a local file - debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE', cache=False) + debug_mode = get_global_setting('REPORT_DEBUG_MODE', cache=False) # Check if the file exists if not filename: @@ -300,7 +300,7 @@ def logo_image(**kwargs): - Otherwise, return a path to the default InvenTree logo """ # If in debug mode, return URL to the image, not a local file - debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE', cache=False) + debug_mode = get_global_setting('REPORT_DEBUG_MODE', cache=False) return InvenTree.helpers.getLogoImage(as_file=not debug_mode, **kwargs) diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index aa1f797f5b04..5b6c6dab18ee 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -32,6 +32,7 @@ import InvenTree.tasks import report.mixins import report.models +from common.settings import get_global_setting from company import models as CompanyModels from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField from order.status_codes import SalesOrderStatusGroups @@ -234,9 +235,7 @@ def check_ownership(self, user): if user.is_superuser: return True - ownership_enabled = common.models.InvenTreeSetting.get_setting( - 'STOCK_OWNERSHIP_CONTROL' - ) + ownership_enabled = get_global_setting('STOCK_OWNERSHIP_CONTROL') if not ownership_enabled: # Location ownership function is not enabled, so return True @@ -310,9 +309,7 @@ def default_delete_on_deplete(): Now, there is a user-configurable setting to govern default behaviour. """ try: - return common.models.InvenTreeSetting.get_setting( - 'STOCK_DELETE_DEPLETED_DEFAULT', True - ) + return get_global_setting('STOCK_DELETE_DEPLETED_DEFAULT', True) except (IntegrityError, OperationalError): # Revert to original default behaviour return True @@ -996,9 +993,7 @@ def check_ownership(self, user): if user.is_superuser: return True - ownership_enabled = common.models.InvenTreeSetting.get_setting( - 'STOCK_OWNERSHIP_CONTROL' - ) + ownership_enabled = get_global_setting('STOCK_OWNERSHIP_CONTROL') if not ownership_enabled: # Location ownership function is not enabled, so return True @@ -1027,7 +1022,7 @@ def is_stale(self): today = InvenTree.helpers.current_date() - stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS') + stale_days = get_global_setting('STOCK_STALE_DAYS') if stale_days <= 0: return False @@ -1897,7 +1892,7 @@ def move(self, location, notes, user, **kwargs): except InvalidOperation: return False - allow_out_of_stock_transfer = common.models.InvenTreeSetting.get_setting( + allow_out_of_stock_transfer = get_global_setting( 'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', backup_value=False, cache=False ) diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index bf8ab2e0ac52..3cfd845803ab 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -1,7 +1,7 @@ """JSON serializers for Stock app.""" import logging -from datetime import datetime, timedelta +from datetime import timedelta from decimal import Decimal from django.core.exceptions import ValidationError as DjangoValidationError @@ -16,7 +16,6 @@ from taggit.serializers import TagListSerializerField import build.models -import common.models import company.models import InvenTree.helpers import InvenTree.serializers @@ -25,6 +24,7 @@ import part.models as part_models import stock.filters import stock.status_codes +from common.settings import get_global_setting from company.serializers import SupplierPartSerializer from InvenTree.serializers import InvenTreeCurrencySerializer, InvenTreeDecimalField from part.serializers import PartBriefSerializer, PartTestTemplateSerializer @@ -476,7 +476,7 @@ def annotate_queryset(queryset): ) # Add flag to indicate if the StockItem is stale - stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS') + stale_days = get_global_setting('STOCK_STALE_DAYS') stale_date = InvenTree.helpers.current_date() + timedelta(days=stale_days) stale_filter = ( StockItem.IN_STOCK_FILTER @@ -730,7 +730,7 @@ def validate_stock_item(self, stock_item): parent_item = self.context['item'] parent_part = parent_item.part - if common.models.InvenTreeSetting.get_setting( + if get_global_setting( 'STOCK_ENFORCE_BOM_INSTALLATION', backup_value=True, cache=False ): # Check if the selected part is in the Bill of Materials of the parent item diff --git a/src/backend/InvenTree/stock/views.py b/src/backend/InvenTree/stock/views.py index 3fb10d968f65..57140a9952f2 100644 --- a/src/backend/InvenTree/stock/views.py +++ b/src/backend/InvenTree/stock/views.py @@ -4,7 +4,7 @@ from django.urls import reverse from django.views.generic import DetailView, ListView -import common.settings +from common.settings import get_global_setting from InvenTree.views import InvenTreeRoleMixin from plugin.views import InvenTreePluginViewMixin @@ -34,9 +34,7 @@ def get_context_data(self, **kwargs): # No 'ownership' checks are necessary for the top-level StockLocation view context['user_owns_location'] = True context['location_owner'] = None - context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting( - 'STOCK_OWNERSHIP_CONTROL' - ) + context['ownership_enabled'] = get_global_setting('STOCK_OWNERSHIP_CONTROL') return context @@ -53,9 +51,7 @@ def get_context_data(self, **kwargs): """Extend template context.""" context = super().get_context_data(**kwargs) - context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting( - 'STOCK_OWNERSHIP_CONTROL' - ) + context['ownership_enabled'] = get_global_setting('STOCK_OWNERSHIP_CONTROL') context['location_owner'] = context['location'].get_location_owner() context['user_owns_location'] = context['location'].check_ownership( self.request.user @@ -80,9 +76,7 @@ def get_context_data(self, **kwargs): data['previous'] = self.object.get_next_serialized_item(reverse=True) data['next'] = self.object.get_next_serialized_item() - data['ownership_enabled'] = common.models.InvenTreeSetting.get_setting( - 'STOCK_OWNERSHIP_CONTROL' - ) + data['ownership_enabled'] = get_global_setting('STOCK_OWNERSHIP_CONTROL') data['item_owner'] = self.object.get_item_owner() data['user_owns_item'] = self.object.check_ownership(self.request.user) diff --git a/src/backend/InvenTree/users/models.py b/src/backend/InvenTree/users/models.py index a544bdec10ba..0a175b3c7a04 100644 --- a/src/backend/InvenTree/users/models.py +++ b/src/backend/InvenTree/users/models.py @@ -21,9 +21,9 @@ from rest_framework.authtoken.models import Token as AuthToken -import common.models as common_models import InvenTree.helpers import InvenTree.models +from common.settings import get_global_setting from InvenTree.ready import canAppAccessDatabase, isImportingData logger = logging.getLogger('inventree') @@ -34,7 +34,7 @@ # string representation of a user def user_model_str(self): """Function to override the default Django User __str__.""" - if common_models.InvenTreeSetting.get_setting('DISPLAY_FULL_NAMES', cache=True): + if get_global_setting('DISPLAY_FULL_NAMES', cache=True): if self.first_name or self.last_name: return f'{self.first_name} {self.last_name}' return self.username @@ -816,11 +816,8 @@ def get_api_url(): # pragma: no cover def __str__(self): """Defines the owner string representation.""" - if ( - self.owner_type.name == 'user' - and common_models.InvenTreeSetting.get_setting( - 'DISPLAY_FULL_NAMES', cache=True - ) + if self.owner_type.name == 'user' and get_global_setting( + 'DISPLAY_FULL_NAMES', cache=True ): display_name = self.owner.get_full_name() else: @@ -829,11 +826,8 @@ def __str__(self): def name(self): """Return the 'name' of this owner.""" - if ( - self.owner_type.name == 'user' - and common_models.InvenTreeSetting.get_setting( - 'DISPLAY_FULL_NAMES', cache=True - ) + if self.owner_type.name == 'user' and get_global_setting( + 'DISPLAY_FULL_NAMES', cache=True ): return self.owner.get_full_name() or str(self.owner) return str(self.owner)