From ec53099872c75ddb82b7f81079b5860f27da5eab Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Jul 2021 01:34:35 +0200 Subject: [PATCH 01/21] abstracting Settings model --- InvenTree/common/models.py | 764 +++++++++++++++++++------------------ 1 file changed, 389 insertions(+), 375 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 3d18d56880..02d752aa8c 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -11,6 +11,7 @@ import decimal import math from django.db import models, transaction +from django.contrib.auth.models import User from django.db.utils import IntegrityError, OperationalError from django.conf import settings @@ -28,7 +29,394 @@ import InvenTree.helpers import InvenTree.fields -class InvenTreeSetting(models.Model): +class BaseInvenTreeSetting(models.Model): + """ + An base InvenTreeSetting object is a key:value pair used for storing + single values (e.g. one-off settings values). + """ + + + GLOBAL_SETTINGS = {} + + class Meta: + abstract = True + + @classmethod + def get_setting_name(cls, key): + """ + Return the name of a particular setting. + + If it does not exist, return an empty string. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('name', '') + else: + return '' + + @classmethod + def get_setting_description(cls, key): + """ + Return the description for a particular setting. + + If it does not exist, return an empty string. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('description', '') + else: + return '' + + @classmethod + def get_setting_units(cls, key): + """ + Return the units for a particular setting. + + If it does not exist, return an empty string. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('units', '') + else: + return '' + + @classmethod + def get_setting_validator(cls, key): + """ + Return the validator for a particular setting. + + If it does not exist, return None + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('validator', None) + else: + return None + + @classmethod + def get_setting_default(cls, key): + """ + Return the default value for a particular setting. + + If it does not exist, return an empty string + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('default', '') + else: + return '' + + @classmethod + def get_setting_choices(cls, key): + """ + Return the validator choices available for a particular setting. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + choices = setting.get('choices', None) + else: + choices = None + + """ + TODO: + if type(choices) is function: + # Evaluate the function (we expect it will return a list of tuples...) + return choices() + """ + + return choices + + @classmethod + def get_setting_object(cls, key): + """ + Return an InvenTreeSetting object matching the given key. + + - Key is case-insensitive + - Returns None if no match is made + """ + + key = str(key).strip().upper() + + try: + setting = cls.objects.filter(key__iexact=key).first() + except (ValueError, cls.DoesNotExist): + setting = None + except (IntegrityError, OperationalError): + setting = None + + # Setting does not exist! (Try to create it) + if not setting: + + setting = cls(key=key, value=cls.get_setting_default(key)) + + try: + # Wrap this statement in "atomic", so it can be rolled back if it fails + with transaction.atomic(): + setting.save() + except (IntegrityError, OperationalError): + # It might be the case that the database isn't created yet + pass + + return setting + + @classmethod + def get_setting_pk(cls, key): + """ + Return the primary-key value for a given setting. + + If the setting does not exist, return None + """ + + setting = cls.get_setting_object(cls) + + if setting: + return setting.pk + else: + return None + + @classmethod + def get_setting(cls, key, backup_value=None): + """ + Get the value of a particular setting. + If it does not exist, return the backup value (default = None) + """ + + # If no backup value is specified, atttempt to retrieve a "default" value + if backup_value is None: + backup_value = cls.get_setting_default(key) + + setting = cls.get_setting_object(key) + + if setting: + value = setting.value + + # If the particular setting is defined as a boolean, cast the value to a boolean + if setting.is_bool(): + value = InvenTree.helpers.str2bool(value) + + if setting.is_int(): + try: + value = int(value) + except (ValueError, TypeError): + value = backup_value + + else: + value = backup_value + + return value + + @classmethod + def set_setting(cls, key, value, user, create=True): + """ + Set the value of a particular setting. + If it does not exist, option to create it. + + Args: + key: settings key + value: New value + user: User object (must be staff member to update a core setting) + create: If True, create a new setting if the specified key does not exist. + """ + + if user is not None and not user.is_staff: + return + + try: + setting = cls.objects.get(key__iexact=key) + except cls.DoesNotExist: + + if create: + setting = cls(key=key) + else: + return + + # Enforce standard boolean representation + if setting.is_bool(): + value = InvenTree.helpers.str2bool(value) + + setting.value = str(value) + setting.save() + + key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive')) + + value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) + + @property + def name(self): + return self.__class__.get_setting_name(self.key) + + @property + def default_value(self): + return self.__class__.get_setting_default(self.key) + + @property + def description(self): + return self.__class__.get_setting_description(self.key) + + @property + def units(self): + return self.__class__.get_setting_units(self.key) + + def clean(self): + """ + If a validator (or multiple validators) are defined for a particular setting key, + run them against the 'value' field. + """ + + super().clean() + + validator = self.__class__.get_setting_validator(self.key) + + if self.is_bool(): + self.value = InvenTree.helpers.str2bool(self.value) + + if self.is_int(): + try: + self.value = int(self.value) + except (ValueError): + raise ValidationError(_('Must be an integer value')) + + if validator is not None: + self.run_validator(validator) + + def run_validator(self, validator): + """ + Run a validator against the 'value' field for this InvenTreeSetting object. + """ + + if validator is None: + return + + value = self.value + + # Boolean validator + if self.is_bool(): + # Value must "look like" a boolean value + if InvenTree.helpers.is_bool(value): + # Coerce into either "True" or "False" + value = InvenTree.helpers.str2bool(value) + else: + raise ValidationError({ + 'value': _('Value must be a boolean value') + }) + + # Integer validator + if self.is_int(): + + try: + # Coerce into an integer value + value = int(value) + except (ValueError, TypeError): + raise ValidationError({ + 'value': _('Value must be an integer value'), + }) + + # If a list of validators is supplied, iterate through each one + if type(validator) in [list, tuple]: + for v in validator: + self.run_validator(v) + + if callable(validator): + # We can accept function validators with a single argument + validator(self.value) + + def validate_unique(self, exclude=None): + """ Ensure that the key:value pair is unique. + In addition to the base validators, this ensures that the 'key' + is unique, using a case-insensitive comparison. + """ + + super().validate_unique(exclude) + + try: + setting = self.__class__.objects.exclude(id=self.id).filter(key__iexact=self.key) + if setting.exists(): + raise ValidationError({'key': _('Key string must be unique')}) + except self.DoesNotExist: + pass + + def choices(self): + """ + Return the available choices for this setting (or None if no choices are defined) + """ + + return self.__class__.get_setting_choices(self.key) + + def is_bool(self): + """ + Check if this setting is required to be a boolean value + """ + + validator = self.__class__.get_setting_validator(self.key) + + if validator == bool: + return True + + if type(validator) in [list, tuple]: + for v in validator: + if v == bool: + return True + + def as_bool(self): + """ + Return the value of this setting converted to a boolean value. + + Warning: Only use on values where is_bool evaluates to true! + """ + + return InvenTree.helpers.str2bool(self.value) + + def is_int(self): + """ + Check if the setting is required to be an integer value: + """ + + validator = self.__class__.get_setting_validator(self.key) + + if validator == int: + return True + + if type(validator) in [list, tuple]: + for v in validator: + if v == int: + return True + + return False + + def as_int(self): + """ + Return the value of this setting converted to a boolean value. + + If an error occurs, return the default value + """ + + try: + value = int(self.value) + except (ValueError, TypeError): + value = self.default_value() + + return value + + +class InvenTreeSetting(BaseInvenTreeSetting): """ An InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values). @@ -357,380 +745,6 @@ class InvenTreeSetting(models.Model): verbose_name = "InvenTree Setting" verbose_name_plural = "InvenTree Settings" - @classmethod - def get_setting_name(cls, key): - """ - Return the name of a particular setting. - - If it does not exist, return an empty string. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('name', '') - else: - return '' - - @classmethod - def get_setting_description(cls, key): - """ - Return the description for a particular setting. - - If it does not exist, return an empty string. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('description', '') - else: - return '' - - @classmethod - def get_setting_units(cls, key): - """ - Return the units for a particular setting. - - If it does not exist, return an empty string. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('units', '') - else: - return '' - - @classmethod - def get_setting_validator(cls, key): - """ - Return the validator for a particular setting. - - If it does not exist, return None - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('validator', None) - else: - return None - - @classmethod - def get_setting_default(cls, key): - """ - Return the default value for a particular setting. - - If it does not exist, return an empty string - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('default', '') - else: - return '' - - @classmethod - def get_setting_choices(cls, key): - """ - Return the validator choices available for a particular setting. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - choices = setting.get('choices', None) - else: - choices = None - - """ - TODO: - if type(choices) is function: - # Evaluate the function (we expect it will return a list of tuples...) - return choices() - """ - - return choices - - @classmethod - def get_setting_object(cls, key): - """ - Return an InvenTreeSetting object matching the given key. - - - Key is case-insensitive - - Returns None if no match is made - """ - - key = str(key).strip().upper() - - try: - setting = InvenTreeSetting.objects.filter(key__iexact=key).first() - except (ValueError, InvenTreeSetting.DoesNotExist): - setting = None - except (IntegrityError, OperationalError): - setting = None - - # Setting does not exist! (Try to create it) - if not setting: - - setting = InvenTreeSetting(key=key, value=InvenTreeSetting.get_setting_default(key)) - - try: - # Wrap this statement in "atomic", so it can be rolled back if it fails - with transaction.atomic(): - setting.save() - except (IntegrityError, OperationalError): - # It might be the case that the database isn't created yet - pass - - return setting - - @classmethod - def get_setting_pk(cls, key): - """ - Return the primary-key value for a given setting. - - If the setting does not exist, return None - """ - - setting = InvenTreeSetting.get_setting_object(cls) - - if setting: - return setting.pk - else: - return None - - @classmethod - def get_setting(cls, key, backup_value=None): - """ - Get the value of a particular setting. - If it does not exist, return the backup value (default = None) - """ - - # If no backup value is specified, atttempt to retrieve a "default" value - if backup_value is None: - backup_value = cls.get_setting_default(key) - - setting = InvenTreeSetting.get_setting_object(key) - - if setting: - value = setting.value - - # If the particular setting is defined as a boolean, cast the value to a boolean - if setting.is_bool(): - value = InvenTree.helpers.str2bool(value) - - if setting.is_int(): - try: - value = int(value) - except (ValueError, TypeError): - value = backup_value - - else: - value = backup_value - - return value - - @classmethod - def set_setting(cls, key, value, user, create=True): - """ - Set the value of a particular setting. - If it does not exist, option to create it. - - Args: - key: settings key - value: New value - user: User object (must be staff member to update a core setting) - create: If True, create a new setting if the specified key does not exist. - """ - - if user is not None and not user.is_staff: - return - - try: - setting = InvenTreeSetting.objects.get(key__iexact=key) - except InvenTreeSetting.DoesNotExist: - - if create: - setting = InvenTreeSetting(key=key) - else: - return - - # Enforce standard boolean representation - if setting.is_bool(): - value = InvenTree.helpers.str2bool(value) - - setting.value = str(value) - setting.save() - - key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive')) - - value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) - - @property - def name(self): - return InvenTreeSetting.get_setting_name(self.key) - - @property - def default_value(self): - return InvenTreeSetting.get_setting_default(self.key) - - @property - def description(self): - return InvenTreeSetting.get_setting_description(self.key) - - @property - def units(self): - return InvenTreeSetting.get_setting_units(self.key) - - def clean(self): - """ - If a validator (or multiple validators) are defined for a particular setting key, - run them against the 'value' field. - """ - - super().clean() - - validator = InvenTreeSetting.get_setting_validator(self.key) - - if self.is_bool(): - self.value = InvenTree.helpers.str2bool(self.value) - - if self.is_int(): - try: - self.value = int(self.value) - except (ValueError): - raise ValidationError(_('Must be an integer value')) - - if validator is not None: - self.run_validator(validator) - - def run_validator(self, validator): - """ - Run a validator against the 'value' field for this InvenTreeSetting object. - """ - - if validator is None: - return - - value = self.value - - # Boolean validator - if self.is_bool(): - # Value must "look like" a boolean value - if InvenTree.helpers.is_bool(value): - # Coerce into either "True" or "False" - value = InvenTree.helpers.str2bool(value) - else: - raise ValidationError({ - 'value': _('Value must be a boolean value') - }) - - # Integer validator - if self.is_int(): - - try: - # Coerce into an integer value - value = int(value) - except (ValueError, TypeError): - raise ValidationError({ - 'value': _('Value must be an integer value'), - }) - - # If a list of validators is supplied, iterate through each one - if type(validator) in [list, tuple]: - for v in validator: - self.run_validator(v) - - if callable(validator): - # We can accept function validators with a single argument - validator(self.value) - - def validate_unique(self, exclude=None): - """ Ensure that the key:value pair is unique. - In addition to the base validators, this ensures that the 'key' - is unique, using a case-insensitive comparison. - """ - - super().validate_unique(exclude) - - try: - setting = InvenTreeSetting.objects.exclude(id=self.id).filter(key__iexact=self.key) - if setting.exists(): - raise ValidationError({'key': _('Key string must be unique')}) - except InvenTreeSetting.DoesNotExist: - pass - - def choices(self): - """ - Return the available choices for this setting (or None if no choices are defined) - """ - - return InvenTreeSetting.get_setting_choices(self.key) - - def is_bool(self): - """ - Check if this setting is required to be a boolean value - """ - - validator = InvenTreeSetting.get_setting_validator(self.key) - - if validator == bool: - return True - - if type(validator) in [list, tuple]: - for v in validator: - if v == bool: - return True - - def as_bool(self): - """ - Return the value of this setting converted to a boolean value. - - Warning: Only use on values where is_bool evaluates to true! - """ - - return InvenTree.helpers.str2bool(self.value) - - def is_int(self): - """ - Check if the setting is required to be an integer value: - """ - - validator = InvenTreeSetting.get_setting_validator(self.key) - - if validator == int: - return True - - if type(validator) in [list, tuple]: - for v in validator: - if v == int: - return True - - return False - - def as_int(self): - """ - Return the value of this setting converted to a boolean value. - - If an error occurs, return the default value - """ - - try: - value = int(self.value) - except (ValueError, TypeError): - value = self.default_value() - - return value - class PriceBreak(models.Model): """ From ce3f7b698d5c1d0010d98b60a1c3df3f8e28eafc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Jul 2021 01:35:58 +0200 Subject: [PATCH 02/21] InvenTreeUserSettings added --- .../migrations/0011_inventreeusersetting.py | 29 +++++++++++++++++++ InvenTree/common/models.py | 27 +++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 InvenTree/common/migrations/0011_inventreeusersetting.py diff --git a/InvenTree/common/migrations/0011_inventreeusersetting.py b/InvenTree/common/migrations/0011_inventreeusersetting.py new file mode 100644 index 0000000000..0bec691041 --- /dev/null +++ b/InvenTree/common/migrations/0011_inventreeusersetting.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.4 on 2021-07-19 22:57 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('common', '0010_migrate_currency_setting'), + ] + + operations = [ + migrations.CreateModel( + name='InvenTreeUserSetting', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50, unique=True)), + ('value', models.CharField(blank=True, help_text='Settings value', max_length=200)), + ('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'InvenTree User Setting', + 'verbose_name_plural': 'InvenTree User Settings', + }, + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 02d752aa8c..8907299a44 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -746,6 +746,33 @@ class InvenTreeSetting(BaseInvenTreeSetting): verbose_name_plural = "InvenTree Settings" +class InvenTreeUserSetting(BaseInvenTreeSetting): + """ + An InvenTreeSetting object with a usercontext + """ + + GLOBAL_SETTINGS = { + 'PART_ASSEMBLY': { + 'name': _('Assembly'), + 'description': _('Parts can be assembled from other components by default'), + 'default': False, + 'validator': bool, + }, + } + + class Meta: + verbose_name = "InvenTree User Setting" + verbose_name_plural = "InvenTree User Settings" + + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + blank=True, null=True, + verbose_name=_('User'), + help_text=_('User'), + ) + + class PriceBreak(models.Model): """ Represents a PriceBreak model From c0d6ef80fc66367fdf018c4b61ce6fa71e426192 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:39:19 +0200 Subject: [PATCH 03/21] unique model settings --- ...usersetting.py => 0011_auto_20210722_2114.py} | 8 ++++++-- InvenTree/common/models.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) rename InvenTree/common/migrations/{0011_inventreeusersetting.py => 0011_auto_20210722_2114.py} (78%) diff --git a/InvenTree/common/migrations/0011_inventreeusersetting.py b/InvenTree/common/migrations/0011_auto_20210722_2114.py similarity index 78% rename from InvenTree/common/migrations/0011_inventreeusersetting.py rename to InvenTree/common/migrations/0011_auto_20210722_2114.py index 0bec691041..388117f261 100644 --- a/InvenTree/common/migrations/0011_inventreeusersetting.py +++ b/InvenTree/common/migrations/0011_auto_20210722_2114.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.4 on 2021-07-19 22:57 +# Generated by Django 3.2.4 on 2021-07-22 21:14 from django.conf import settings from django.db import migrations, models @@ -17,8 +17,8 @@ class Migration(migrations.Migration): name='InvenTreeUserSetting', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50, unique=True)), ('value', models.CharField(blank=True, help_text='Settings value', max_length=200)), + ('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50)), ('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), ], options={ @@ -26,4 +26,8 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'InvenTree User Settings', }, ), + migrations.AddConstraint( + model_name='inventreeusersetting', + constraint=models.UniqueConstraint(fields=('key', 'user'), name='unique key and user'), + ), ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 8907299a44..bdaa73b142 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -745,6 +745,12 @@ class InvenTreeSetting(BaseInvenTreeSetting): verbose_name = "InvenTree Setting" verbose_name_plural = "InvenTree Settings" + key = models.CharField( + max_length=50, + blank=False, + unique=True, + help_text=_('Settings key (must be unique - case insensitive'), + ) class InvenTreeUserSetting(BaseInvenTreeSetting): """ @@ -763,6 +769,16 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): class Meta: verbose_name = "InvenTree User Setting" verbose_name_plural = "InvenTree User Settings" + constraints = [ + models.UniqueConstraint(fields=['key', 'user'], name='unique key and user') + ] + + key = models.CharField( + max_length=50, + blank=False, + unique=False, + help_text=_('Settings key (must be unique - case insensitive'), + ) user = models.ForeignKey( User, From 69ff0ac248ef475594c6d743e703acf81853b7a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:39:36 +0200 Subject: [PATCH 04/21] ruleset --- InvenTree/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 73bc5b6695..179a70ed74 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -141,6 +141,7 @@ class RuleSet(models.Model): # Models which currently do not require permissions 'common_colortheme', 'common_inventreesetting', + 'common_inventreeusersetting', 'company_contact', 'users_owner', From 8f374e255e13e92e76abbe61082908a610aa6116 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:43:03 +0200 Subject: [PATCH 05/21] abstract filters and refactor --- InvenTree/common/models.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index bdaa73b142..dca371a671 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -145,7 +145,11 @@ class BaseInvenTreeSetting(models.Model): return choices @classmethod - def get_setting_object(cls, key): + def get_filters(cls, key, **kwargs): + return {'key__iexact': key} + + @classmethod + def get_setting_object(cls, key, **kwargs): """ Return an InvenTreeSetting object matching the given key. @@ -156,7 +160,7 @@ class BaseInvenTreeSetting(models.Model): key = str(key).strip().upper() try: - setting = cls.objects.filter(key__iexact=key).first() + setting = cls.objects.filter(**cls.get_filters(key, **kwargs)).first() except (ValueError, cls.DoesNotExist): setting = None except (IntegrityError, OperationalError): @@ -165,7 +169,7 @@ class BaseInvenTreeSetting(models.Model): # Setting does not exist! (Try to create it) if not setting: - setting = cls(key=key, value=cls.get_setting_default(key)) + setting = cls(key=key, value=cls.get_setting_default(key), **kwargs) try: # Wrap this statement in "atomic", so it can be rolled back if it fails @@ -224,7 +228,7 @@ class BaseInvenTreeSetting(models.Model): return value @classmethod - def set_setting(cls, key, value, user, create=True): + def set_setting(cls, key, value, change_user, create=True, **kwargs): """ Set the value of a particular setting. If it does not exist, option to create it. @@ -232,19 +236,19 @@ class BaseInvenTreeSetting(models.Model): Args: key: settings key value: New value - user: User object (must be staff member to update a core setting) + change_user: User object (must be staff member to update a core setting) create: If True, create a new setting if the specified key does not exist. """ - if user is not None and not user.is_staff: + if change_user is not None and not change_user.is_staff: return try: - setting = cls.objects.get(key__iexact=key) + setting = cls.objects.get(**cls.get_filters(key, **kwargs)) except cls.DoesNotExist: if create: - setting = cls(key=key) + setting = cls(key=key, **kwargs) else: return @@ -338,7 +342,7 @@ class BaseInvenTreeSetting(models.Model): # We can accept function validators with a single argument validator(self.value) - def validate_unique(self, exclude=None): + def validate_unique(self, exclude=None, **kwargs): """ Ensure that the key:value pair is unique. In addition to the base validators, this ensures that the 'key' is unique, using a case-insensitive comparison. @@ -347,7 +351,7 @@ class BaseInvenTreeSetting(models.Model): super().validate_unique(exclude) try: - setting = self.__class__.objects.exclude(id=self.id).filter(key__iexact=self.key) + setting = self.__class__.objects.exclude(id=self.id).filter(**self.get_filters(self.key, **kwargs)) if setting.exists(): raise ValidationError({'key': _('Key string must be unique')}) except self.DoesNotExist: From 6f5fc528b7e0ff4f3edf5883070f0eec29c6e4f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:43:51 +0200 Subject: [PATCH 06/21] override functions --- InvenTree/common/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index dca371a671..7072bc86d4 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -792,6 +792,17 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): help_text=_('User'), ) + @classmethod + def get_setting_object(cls, key, user): + return super().get_setting_object(key, user=user) + + def validate_unique(self, exclude=None): + return super().validate_unique(exclude=exclude, user=self.user) + + @classmethod + def get_filters(cls, key, **kwargs): + return {'key__iexact': key, 'user__id__iexact': kwargs['user'].id} + class PriceBreak(models.Model): """ From 7ef87320a047a3610d4ca83dca56cfc5e21bc6c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:44:25 +0200 Subject: [PATCH 07/21] abstract edit --- InvenTree/common/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index f953dffa81..f182b03e0d 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -45,8 +45,8 @@ class SettingEdit(AjaxUpdateView): ctx['key'] = setting.key ctx['value'] = setting.value - ctx['name'] = models.InvenTreeSetting.get_setting_name(setting.key) - ctx['description'] = models.InvenTreeSetting.get_setting_description(setting.key) + ctx['name'] = self.model.get_setting_name(setting.key) + ctx['description'] = self.model.get_setting_description(setting.key) return ctx @@ -69,12 +69,12 @@ class SettingEdit(AjaxUpdateView): self.object.value = str2bool(setting.value) form.fields['value'].value = str2bool(setting.value) - name = models.InvenTreeSetting.get_setting_name(setting.key) + name = self.model.get_setting_name(setting.key) if name: form.fields['value'].label = name - description = models.InvenTreeSetting.get_setting_description(setting.key) + description = self.model.get_setting_description(setting.key) if description: form.fields['value'].help_text = description From 449fc329c971e290f1cd7efc021c11be7de507a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:45:34 +0200 Subject: [PATCH 08/21] usersetting edit url --- InvenTree/InvenTree/urls.py | 3 ++- InvenTree/common/views.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index a3af143f92..a4a4d21807 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -43,7 +43,7 @@ from .views import CurrencySettingsView, CurrencyRefreshView from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView -from common.views import SettingEdit +from common.views import SettingEdit, UserSettingEdit from .api import InfoView, NotFoundView from .api import ActionPluginView @@ -94,6 +94,7 @@ settings_urls = [ url(r'^currencies/', CurrencySettingsView.as_view(), name='settings-currencies'), url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'), + url(r'^(?P\d+)/edit/user', UserSettingEdit.as_view(), name='user-setting-edit'), url(r'^(?P\d+)/edit/', SettingEdit.as_view(), name='setting-edit'), # Catch any other urls diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index f182b03e0d..75fc78b4e3 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -111,6 +111,18 @@ class SettingEdit(AjaxUpdateView): form.add_error('value', _('Supplied value must be a boolean')) +class UserSettingEdit(SettingEdit): + """ + View for editing an InvenTree key:value user settings object, + (or creating it if the key does not already exist) + """ + + model = models.InvenTreeUserSetting + ajax_form_title = _('Change User Setting') + form_class = forms.SettingEditForm + ajax_template_name = "common/edit_setting.html" + + class MultiStepFormView(SessionWizardView): """ Setup basic methods of multi-step form From e287860e102491b3f5c789fc53cd83eeb5e66a66 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:46:31 +0200 Subject: [PATCH 09/21] admin for user setting --- InvenTree/common/admin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index c2da1ddd63..1eda18e869 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -5,7 +5,7 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from .models import InvenTreeSetting +from .models import InvenTreeSetting, InvenTreeUserSetting class SettingsAdmin(ImportExportModelAdmin): @@ -13,4 +13,10 @@ class SettingsAdmin(ImportExportModelAdmin): list_display = ('key', 'value') +class UserSettingsAdmin(ImportExportModelAdmin): + + list_display = ('key', 'value', 'user', ) + + admin.site.register(InvenTreeSetting, SettingsAdmin) +admin.site.register(InvenTreeUserSetting, UserSettingsAdmin) From 3f6c7df7a8e6aa3635df6c053634ad676618270a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:48:28 +0200 Subject: [PATCH 10/21] change template setting behaviour for user setting --- InvenTree/part/templatetags/inventree_extras.py | 9 +++++---- InvenTree/templates/InvenTree/settings/setting.html | 9 +++++++-- InvenTree/templates/InvenTree/settings/settings.html | 7 ++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 38689df26b..8c28d997f6 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -18,7 +18,7 @@ from InvenTree import version, settings import InvenTree.helpers -from common.models import InvenTreeSetting, ColorTheme +from common.models import InvenTreeSetting, ColorTheme, InvenTreeUserSetting from common.settings import currency_code_default register = template.Library() @@ -182,11 +182,12 @@ def setting_object(key, *args, **kwargs): """ Return a setting object speciifed by the given key (Or return None if the setting does not exist) + if a user-setting was requested return that """ - setting = InvenTreeSetting.get_setting_object(key) - - return setting + if 'user' in kwargs: + return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user']) + return InvenTreeSetting.get_setting_object(key) @register.simple_tag() diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html index 7376adf8d9..66f3f9f3b0 100644 --- a/InvenTree/templates/InvenTree/settings/setting.html +++ b/InvenTree/templates/InvenTree/settings/setting.html @@ -1,7 +1,12 @@ {% load inventree_extras %} {% load i18n %} -{% setting_object key as setting %} +{% if user_setting %} + {% setting_object key user=request.user as setting %} +{% else %} + {% setting_object key as setting %} +{% endif %} + {% if icon %} @@ -28,7 +33,7 @@
-
diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index 25ae5d24b2..03cf276594 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -45,9 +45,14 @@ $('table').find('.btn-edit-setting').click(function() { var setting = $(this).attr('setting'); var pk = $(this).attr('pk'); + var url = `/settings/${pk}/edit/`; + + if ($(this).attr('user')){ + url += `user/`; + } launchModalForm( - `/settings/${pk}/edit/`, + url, { reload: true, } From 5f2bef7ee16a58f72f0bb43310c076675a5752a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:50:09 +0200 Subject: [PATCH 11/21] base implementation of user setting --- InvenTree/common/models.py | 8 ++++---- InvenTree/templates/InvenTree/settings/appearance.html | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 7072bc86d4..2e8678b600 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -762,10 +762,10 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): """ GLOBAL_SETTINGS = { - 'PART_ASSEMBLY': { - 'name': _('Assembly'), - 'description': _('Parts can be assembled from other components by default'), - 'default': False, + 'HOMEPAGE_PART_STARRED': { + 'name': _('Show starred parts'), + 'description': _('Show starred parts on the homepage'), + 'default': True, 'validator': bool, }, } diff --git a/InvenTree/templates/InvenTree/settings/appearance.html b/InvenTree/templates/InvenTree/settings/appearance.html index 8cd7a8d2db..6f4f406c62 100644 --- a/InvenTree/templates/InvenTree/settings/appearance.html +++ b/InvenTree/templates/InvenTree/settings/appearance.html @@ -63,5 +63,13 @@ +

{% trans "User Settings" %}

+ + + {% include "InvenTree/settings/header.html" %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} + +
{% endblock %} From 3b12b0231e77f4a366032abe0e382415e01c2af1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:50:51 +0200 Subject: [PATCH 12/21] fixing wired unique behaviour --- InvenTree/common/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 2e8678b600..a07bb1c43f 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -259,7 +259,7 @@ class BaseInvenTreeSetting(models.Model): setting.value = str(value) setting.save() - key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive')) + key = models.CharField(max_length=50, blank=False, unique=False, help_text=_('Settings key (must be unique - case insensitive')) value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) From 7abf70fdd7095695cbe0d8dd3e15e50051008e24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:53:17 +0200 Subject: [PATCH 13/21] style fix --- InvenTree/common/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index a07bb1c43f..0b5f1cabef 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -35,7 +35,6 @@ class BaseInvenTreeSetting(models.Model): single values (e.g. one-off settings values). """ - GLOBAL_SETTINGS = {} class Meta: @@ -756,6 +755,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): help_text=_('Settings key (must be unique - case insensitive'), ) + class InvenTreeUserSetting(BaseInvenTreeSetting): """ An InvenTreeSetting object with a usercontext From e167f27258a8f4fce34ca74e53809bb431f163da Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Jul 2021 00:46:48 +0200 Subject: [PATCH 14/21] get user settings in templates --- InvenTree/common/models.py | 4 ++-- InvenTree/part/templatetags/inventree_extras.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index eebb566a68..48c99cbbb5 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -194,7 +194,7 @@ class BaseInvenTreeSetting(models.Model): return None @classmethod - def get_setting(cls, key, backup_value=None): + def get_setting(cls, key, backup_value=None, **kwargs): """ Get the value of a particular setting. If it does not exist, return the backup value (default = None) @@ -204,7 +204,7 @@ class BaseInvenTreeSetting(models.Model): if backup_value is None: backup_value = cls.get_setting_default(key) - setting = cls.get_setting_object(key) + setting = cls.get_setting_object(key, **kwargs) if setting: value = setting.value diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 8c28d997f6..e867441b42 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -196,6 +196,8 @@ def settings_value(key, *args, **kwargs): Return a settings value specified by the given key """ + if 'user' in kwargs: + return InvenTreeUserSetting.get_setting(key, user=kwargs['user']) return InvenTreeSetting.get_setting(key) From 46b0db826318538c23de86eb96c91ef61d6c1350 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Jul 2021 01:07:28 +0200 Subject: [PATCH 15/21] more hompage settings --- InvenTree/common/models.py | 12 ++++++++ InvenTree/templates/InvenTree/index.html | 29 ++++++++++++------- .../InvenTree/settings/appearance.html | 3 ++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 48c99cbbb5..3ea2e11a44 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -773,6 +773,18 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'default': True, 'validator': bool, }, + 'HOMEPAGE_PART_LATEST': { + 'name': _('Show latest parts'), + 'description': _('Show latest parts on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_BOM_VALIDATION': { + 'name': _('Show starunvalidated BOMs'), + 'description': _('Show BOMs that await validation on the homepage'), + 'default': True, + 'validator': bool, + }, } class Meta: diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index a3d793dd26..aa5272760f 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -95,11 +95,21 @@ function addHeaderAction(label, title, icon, options) { {% if roles.part.view %} addHeaderTitle('{% trans "Parts" %}'); + +{% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %} +{% if setting_part_starred %} addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star'); +loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", { + params: { + "starred": true, + }, + name: 'starred_parts', +}); +{% endif %} + +{% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %} +{% if setting_part_latest %} addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper'); -addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle'); - - loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { params: { ordering: "-creation_date", @@ -107,21 +117,18 @@ loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { }, name: 'latest_parts', }); +{% endif %} -loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", { - params: { - "starred": true, - }, - name: 'starred_parts', -}); - +{% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %} +{% if setting_bom_validation %} +addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle'); loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { params: { "bom_valid": false, }, name: 'bom_invalid_parts', }); - +{% endif %} {% endif %} {% if roles.stock.view %} diff --git a/InvenTree/templates/InvenTree/settings/appearance.html b/InvenTree/templates/InvenTree/settings/appearance.html index 6f4f406c62..43f1242cbf 100644 --- a/InvenTree/templates/InvenTree/settings/appearance.html +++ b/InvenTree/templates/InvenTree/settings/appearance.html @@ -69,6 +69,9 @@ {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %} + From e97ee95debdc00d1a85e69ca1891e986a0cfa03a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Jul 2021 01:09:06 +0200 Subject: [PATCH 16/21] typo --- InvenTree/common/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 3ea2e11a44..faa090d49b 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -780,7 +780,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'validator': bool, }, 'HOMEPAGE_BOM_VALIDATION': { - 'name': _('Show starunvalidated BOMs'), + 'name': _('Show unvalidated BOMs'), 'description': _('Show BOMs that await validation on the homepage'), 'default': True, 'validator': bool, From 32eace0c36e110bd7d68faa2b17f4ee35ae82a7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Jul 2021 11:05:41 +0200 Subject: [PATCH 17/21] moving settings into own section --- InvenTree/InvenTree/urls.py | 1 + .../InvenTree/settings/appearance.html | 13 ---------- .../templates/InvenTree/settings/tabs.html | 3 +++ .../InvenTree/settings/user_settings.html | 25 +++++++++++++++++++ 4 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 InvenTree/templates/InvenTree/settings/user_settings.html diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 029bf597f2..3cc50bc889 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -79,6 +79,7 @@ apipatterns = [ settings_urls = [ + url(r'^usersettings/', SettingsView.as_view(template_name='InvenTree/settings/user_settings.html'), name='settings-user-settings'), url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'), url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'), url(r'^i18n/?', include('django.conf.urls.i18n')), diff --git a/InvenTree/templates/InvenTree/settings/appearance.html b/InvenTree/templates/InvenTree/settings/appearance.html index 43f1242cbf..d0b414e423 100644 --- a/InvenTree/templates/InvenTree/settings/appearance.html +++ b/InvenTree/templates/InvenTree/settings/appearance.html @@ -62,17 +62,4 @@ - -

{% trans "User Settings" %}

- - - {% include "InvenTree/settings/header.html" %} - - {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} - {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" user_setting=True %} - {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %} - - -
- {% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index 86a88b68f0..0e5554ca3e 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -8,6 +8,9 @@ {% trans "Appearance" %} + + {% trans "User Settings" %} + {% if user.is_staff %}

{% trans "InvenTree Settings" %}

diff --git a/InvenTree/templates/InvenTree/settings/user_settings.html b/InvenTree/templates/InvenTree/settings/user_settings.html new file mode 100644 index 0000000000..d56cb5b35a --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/user_settings.html @@ -0,0 +1,25 @@ +{% extends "InvenTree/settings/settings.html" %} +{% load i18n %} +{% load inventree_extras %} + +{% block tabs %} +{% include "InvenTree/settings/tabs.html" with tab='user_settings' %} +{% endblock %} + +{% block subtitle %} +{% trans "User Settings" %} +{% endblock %} + +{% block settings %} + +
+ + {% include "InvenTree/settings/header.html" %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %} + +
+
+{% endblock %} From 31050f23aaa51c77aa764b632d4fd9684c2f5b5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Jul 2021 00:08:46 +0200 Subject: [PATCH 18/21] adding all homepage settings --- InvenTree/common/models.py | 72 ++++++++++++ InvenTree/templates/InvenTree/index.html | 109 ++++++++++++------ .../InvenTree/settings/user_settings.html | 16 +++ 3 files changed, 164 insertions(+), 33 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index faa090d49b..b97bb539e3 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -785,6 +785,78 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'default': True, 'validator': bool, }, + 'HOMEPAGE_STOCK_RECENT': { + 'name': _('Show recent stock changes'), + 'description': _('Show recently changed stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_LOW': { + 'name': _('Show low stock'), + 'description': _('Show low stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_DEPLETED': { + 'name': _('Show depleted stock'), + 'description': _('Show depleted stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_NEEDED': { + 'name': _('Show needed stock'), + 'description': _('Show stock items needed for builds on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_EXPIRED': { + 'name': _('Show expired stock'), + 'description': _('Show expired stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_STALE': { + 'name': _('Show stale stock'), + 'description': _('Show stale stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_BUILD_PENDING': { + 'name': _('Show pending builds'), + 'description': _('Show pending builds on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_BUILD_OVERDUE': { + 'name': _('Show overdue builds'), + 'description': _('Show overdue builds on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_PO_OUTSTANDING': { + 'name': _('Show outstanding POs'), + 'description': _('Show outstanding POs on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_PO_OVERDUE': { + 'name': _('Show overdue POs'), + 'description': _('Show overdue POs on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_SO_OUTSTANDING': { + 'name': _('Show outstanding SOs'), + 'description': _('Show outstanding SOs on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_SO_OVERDUE': { + 'name': _('Show overdue SOs'), + 'description': _('Show overdue SOs on the homepage'), + 'default': True, + 'validator': bool, + }, } class Meta: diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index aa5272760f..65034a8ac8 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -93,6 +93,7 @@ function addHeaderAction(label, title, icon, options) { }); } + {% if roles.part.view %} addHeaderTitle('{% trans "Parts" %}'); @@ -131,13 +132,13 @@ loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { {% endif %} {% endif %} + {% if roles.stock.view %} addHeaderTitle('{% trans "Stock" %}'); -addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock'); -addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart'); -addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times'); -addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn'); +{% settings_value 'HOMEPAGE_STOCK_RECENT' user=request.user as setting_stock_recent %} +{% if setting_stock_recent %} +addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock'); loadStockTable($('#table-recently-updated-stock'), { params: { part_detail: true, @@ -147,12 +148,48 @@ loadStockTable($('#table-recently-updated-stock'), { name: 'recently-updated-stock', grouping: false, }); +{% endif %} + +{% settings_value 'HOMEPAGE_STOCK_LOW' user=request.user as setting_stock_low %} +{% if setting_stock_low %} +addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart'); +loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", { + params: { + low_stock: true, + }, + name: "low_stock_parts", +}); +{% endif %} + +{% settings_value 'HOMEPAGE_STOCK_DEPLETED' user=request.user as setting_stock_depleted %} +{% if setting_stock_depleted %} +addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times'); +loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", { + params: { + depleted_stock: true, + }, + name: "depleted_stock_parts", +}); +{% endif %} + +{% settings_value 'HOMEPAGE_STOCK_NEEDED' user=request.user as setting_stock_needed %} +{% if setting_stock_needed %} +addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn'); +loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", { + params: { + stock_to_build: true, + }, + name: "to_build_parts", +}); +{% endif %} + {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} {% if expiry %} -addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times'); -addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch'); +{% settings_value 'HOMEPAGE_STOCK_EXPIRED' user=request.user as setting_stock_expired %} +{% if setting_stock_expired %} +addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times'); loadStockTable($("#table-expired-stock"), { params: { expired: true, @@ -160,7 +197,11 @@ loadStockTable($("#table-expired-stock"), { part_detail: true, }, }); +{% endif %} +{% settings_value 'HOMEPAGE_STOCK_STALE' user=request.user as setting_stock_stale %} +{% if setting_stock_stale %} +addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch'); loadStockTable($("#table-stale-stock"), { params: { stale: true, @@ -171,34 +212,16 @@ loadStockTable($("#table-stale-stock"), { }); {% endif %} -loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", { - params: { - low_stock: true, - }, - name: "low_stock_parts", -}); - -loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", { - params: { - depleted_stock: true, - }, - name: "depleted_stock_parts", -}); - -loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", { - params: { - stock_to_build: true, - }, - name: "to_build_parts", -}); - {% endif %} +{% endif %} + {% if roles.build.view %} addHeaderTitle('{% trans "Build Orders" %}'); -addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs'); -addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times'); +{% settings_value 'HOMEPAGE_BUILD_PENDING' user=request.user as setting_build_pending %} +{% if setting_build_pending %} +addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs'); loadBuildTable("#table-build-pending", { url: "{% url 'api-build-list' %}", params: { @@ -206,7 +229,11 @@ loadBuildTable("#table-build-pending", { }, disableFilters: true, }); +{% endif %} +{% settings_value 'HOMEPAGE_BUILD_OVERDUE' user=request.user as setting_build_overdue %} +{% if setting_build_overdue %} +addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times'); loadBuildTable("#table-build-overdue", { url: "{% url 'api-build-list' %}", params: { @@ -216,11 +243,15 @@ loadBuildTable("#table-build-overdue", { }); {% endif %} +{% endif %} + + {% if roles.purchase_order.view %} addHeaderTitle('{% trans "Purchase Orders" %}'); -addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt'); -addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times'); +{% settings_value 'HOMEPAGE_PO_OUTSTANDING' user=request.user as setting_po_outstanding %} +{% if setting_po_outstanding %} +addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt'); loadPurchaseOrderTable("#table-po-outstanding", { url: "{% url 'api-po-list' %}", params: { @@ -228,7 +259,11 @@ loadPurchaseOrderTable("#table-po-outstanding", { outstanding: true, } }); +{% endif %} +{% settings_value 'HOMEPAGE_PO_OVERDUE' user=request.user as setting_po_overdue %} +{% if setting_po_overdue %} +addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times'); loadPurchaseOrderTable("#table-po-overdue", { url: "{% url 'api-po-list' %}", params: { @@ -236,14 +271,17 @@ loadPurchaseOrderTable("#table-po-overdue", { overdue: true, } }); +{% endif %} {% endif %} + {% if roles.sales_order.view %} addHeaderTitle('{% trans "Sales Orders" %}'); -addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt'); -addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times'); +{% settings_value 'HOMEPAGE_SO_OUTSTANDING' user=request.user as setting_so_outstanding %} +{% if setting_so_outstanding %} +addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt'); loadSalesOrderTable("#table-so-outstanding", { url: "{% url 'api-so-list' %}", params: { @@ -251,7 +289,11 @@ loadSalesOrderTable("#table-so-outstanding", { outstanding: true, }, }); +{% endif %} +{% settings_value 'HOMEPAGE_SO_OVERDUE' user=request.user as setting_so_overdue %} +{% if setting_so_overdue %} +addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times'); loadSalesOrderTable("#table-so-overdue", { url: "{% url 'api-so-list' %}", params: { @@ -259,6 +301,7 @@ loadSalesOrderTable("#table-so-overdue", { customer_detail: true, } }); +{% endif %} {% endif %} diff --git a/InvenTree/templates/InvenTree/settings/user_settings.html b/InvenTree/templates/InvenTree/settings/user_settings.html index d56cb5b35a..0b76b930c5 100644 --- a/InvenTree/templates/InvenTree/settings/user_settings.html +++ b/InvenTree/templates/InvenTree/settings/user_settings.html @@ -19,6 +19,22 @@ {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" user_setting=True %} {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_RECENT" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_LOW" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_DEPLETED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_NEEDED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_EXPIRED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_STALE" user_setting=True %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BUILD_PENDING" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BUILD_OVERDUE" user_setting=True %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PO_OUTSTANDING" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PO_OVERDUE" user_setting=True %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_SO_OUTSTANDING" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_SO_OVERDUE" user_setting=True %} From 431b35ed322603a73e3875e2496ea4378399e1a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Jul 2021 00:42:17 +0200 Subject: [PATCH 19/21] new tag for building lists --- InvenTree/part/templatetags/inventree_extras.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index e867441b42..a7887ec250 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -69,6 +69,12 @@ def add(x, y, *args, **kwargs): return x + y +@register.simple_tag() +def to_list(*args): + """ Return the input arguments as list """ + return args + + @register.simple_tag() def part_allocation_count(build, part, *args, **kwargs): """ Return the total number of allocated to """ From a82483dbaa7833181fccd4abcae999ac4ddd05f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Jul 2021 00:44:16 +0200 Subject: [PATCH 20/21] hiding homepage block when no setting is used --- InvenTree/templates/InvenTree/index.html | 51 ++++++++++++++---------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index 65034a8ac8..ee08777502 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -93,11 +93,14 @@ function addHeaderAction(label, title, icon, options) { }); } +{% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %} +{% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %} +{% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %} +{% to_list setting_part_starred setting_part_latest setting_bom_validation as settings_list_part %} -{% if roles.part.view %} +{% if roles.part.view and True in settings_list_part %} addHeaderTitle('{% trans "Parts" %}'); -{% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %} {% if setting_part_starred %} addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star'); loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", { @@ -108,7 +111,6 @@ loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", { }); {% endif %} -{% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %} {% if setting_part_latest %} addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper'); loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { @@ -120,7 +122,6 @@ loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { }); {% endif %} -{% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %} {% if setting_bom_validation %} addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle'); loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { @@ -132,11 +133,22 @@ loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { {% endif %} {% endif %} +{% settings_value 'HOMEPAGE_STOCK_RECENT' user=request.user as setting_stock_recent %} +{% settings_value 'HOMEPAGE_STOCK_LOW' user=request.user as setting_stock_low %} +{% settings_value 'HOMEPAGE_STOCK_DEPLETED' user=request.user as setting_stock_depleted %} +{% settings_value 'HOMEPAGE_STOCK_NEEDED' user=request.user as setting_stock_needed %} +{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} +{% if expiry %} +{% settings_value 'HOMEPAGE_STOCK_EXPIRED' user=request.user as setting_stock_expired %} +{% settings_value 'HOMEPAGE_STOCK_STALE' user=request.user as setting_stock_stale %} +{% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed setting_stock_expired setting_stock_stale as settings_list_stock %} +{% else %} +{% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed as settings_list_stock %} +{% endif %} -{% if roles.stock.view %} +{% if roles.stock.view and True in settings_list_stock %} addHeaderTitle('{% trans "Stock" %}'); -{% settings_value 'HOMEPAGE_STOCK_RECENT' user=request.user as setting_stock_recent %} {% if setting_stock_recent %} addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock'); loadStockTable($('#table-recently-updated-stock'), { @@ -150,7 +162,6 @@ loadStockTable($('#table-recently-updated-stock'), { }); {% endif %} -{% settings_value 'HOMEPAGE_STOCK_LOW' user=request.user as setting_stock_low %} {% if setting_stock_low %} addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart'); loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", { @@ -161,7 +172,6 @@ loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", { }); {% endif %} -{% settings_value 'HOMEPAGE_STOCK_DEPLETED' user=request.user as setting_stock_depleted %} {% if setting_stock_depleted %} addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times'); loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", { @@ -172,7 +182,6 @@ loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", { }); {% endif %} -{% settings_value 'HOMEPAGE_STOCK_NEEDED' user=request.user as setting_stock_needed %} {% if setting_stock_needed %} addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn'); loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", { @@ -184,10 +193,8 @@ loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", { {% endif %} -{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} {% if expiry %} -{% settings_value 'HOMEPAGE_STOCK_EXPIRED' user=request.user as setting_stock_expired %} {% if setting_stock_expired %} addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times'); loadStockTable($("#table-expired-stock"), { @@ -199,7 +206,6 @@ loadStockTable($("#table-expired-stock"), { }); {% endif %} -{% settings_value 'HOMEPAGE_STOCK_STALE' user=request.user as setting_stock_stale %} {% if setting_stock_stale %} addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch'); loadStockTable($("#table-stale-stock"), { @@ -215,11 +221,13 @@ loadStockTable($("#table-stale-stock"), { {% endif %} {% endif %} +{% settings_value 'HOMEPAGE_BUILD_PENDING' user=request.user as setting_build_pending %} +{% settings_value 'HOMEPAGE_BUILD_OVERDUE' user=request.user as setting_build_overdue %} +{% to_list setting_build_pending setting_build_overdue as settings_list_build %} -{% if roles.build.view %} +{% if roles.build.view and True in settings_list_build %} addHeaderTitle('{% trans "Build Orders" %}'); -{% settings_value 'HOMEPAGE_BUILD_PENDING' user=request.user as setting_build_pending %} {% if setting_build_pending %} addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs'); loadBuildTable("#table-build-pending", { @@ -231,7 +239,6 @@ loadBuildTable("#table-build-pending", { }); {% endif %} -{% settings_value 'HOMEPAGE_BUILD_OVERDUE' user=request.user as setting_build_overdue %} {% if setting_build_overdue %} addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times'); loadBuildTable("#table-build-overdue", { @@ -245,11 +252,13 @@ loadBuildTable("#table-build-overdue", { {% endif %} +{% settings_value 'HOMEPAGE_PO_OUTSTANDING' user=request.user as setting_po_outstanding %} +{% settings_value 'HOMEPAGE_PO_OVERDUE' user=request.user as setting_po_overdue %} +{% to_list setting_po_outstanding setting_po_overdue as settings_list_po %} -{% if roles.purchase_order.view %} +{% if roles.purchase_order.view and True in settings_list_po %} addHeaderTitle('{% trans "Purchase Orders" %}'); -{% settings_value 'HOMEPAGE_PO_OUTSTANDING' user=request.user as setting_po_outstanding %} {% if setting_po_outstanding %} addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt'); loadPurchaseOrderTable("#table-po-outstanding", { @@ -261,7 +270,6 @@ loadPurchaseOrderTable("#table-po-outstanding", { }); {% endif %} -{% settings_value 'HOMEPAGE_PO_OVERDUE' user=request.user as setting_po_overdue %} {% if setting_po_overdue %} addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times'); loadPurchaseOrderTable("#table-po-overdue", { @@ -275,11 +283,13 @@ loadPurchaseOrderTable("#table-po-overdue", { {% endif %} +{% settings_value 'HOMEPAGE_SO_OUTSTANDING' user=request.user as setting_so_outstanding %} +{% settings_value 'HOMEPAGE_SO_OVERDUE' user=request.user as setting_so_overdue %} +{% to_list setting_so_outstanding setting_so_overdue as settings_list_so %} -{% if roles.sales_order.view %} +{% if roles.sales_order.view and True in settings_list_so %} addHeaderTitle('{% trans "Sales Orders" %}'); -{% settings_value 'HOMEPAGE_SO_OUTSTANDING' user=request.user as setting_so_outstanding %} {% if setting_so_outstanding %} addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt'); loadSalesOrderTable("#table-so-outstanding", { @@ -291,7 +301,6 @@ loadSalesOrderTable("#table-so-outstanding", { }); {% endif %} -{% settings_value 'HOMEPAGE_SO_OVERDUE' user=request.user as setting_so_overdue %} {% if setting_so_overdue %} addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times'); loadSalesOrderTable("#table-so-overdue", { From 175b24a79470e52ffc907b8d13f0e9c847352a72 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Jul 2021 00:45:37 +0200 Subject: [PATCH 21/21] changing user settings icon --- InvenTree/templates/InvenTree/settings/tabs.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index 0e5554ca3e..846dfccd23 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -9,7 +9,7 @@ {% trans "Appearance" %} - {% trans "User Settings" %} + {% trans "User Settings" %} {% if user.is_staff %}