mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
125260160c
@ -43,7 +43,7 @@ from .views import CurrencySettingsView, CurrencyRefreshView
|
|||||||
from .views import AppearanceSelectView, SettingCategorySelectView
|
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||||
from .views import DynamicJsView
|
from .views import DynamicJsView
|
||||||
|
|
||||||
from common.views import SettingEdit
|
from common.views import SettingEdit, UserSettingEdit
|
||||||
|
|
||||||
from .api import InfoView, NotFoundView
|
from .api import InfoView, NotFoundView
|
||||||
from .api import ActionPluginView
|
from .api import ActionPluginView
|
||||||
@ -79,6 +79,7 @@ apipatterns = [
|
|||||||
|
|
||||||
settings_urls = [
|
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'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
||||||
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||||
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
||||||
@ -94,6 +95,7 @@ settings_urls = [
|
|||||||
url(r'^currencies/', CurrencySettingsView.as_view(), name='settings-currencies'),
|
url(r'^currencies/', CurrencySettingsView.as_view(), name='settings-currencies'),
|
||||||
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
||||||
|
|
||||||
|
url(r'^(?P<pk>\d+)/edit/user', UserSettingEdit.as_view(), name='user-setting-edit'),
|
||||||
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
|
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
|
||||||
|
|
||||||
# Catch any other urls
|
# Catch any other urls
|
||||||
|
@ -5,7 +5,7 @@ from django.contrib import admin
|
|||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
from .models import InvenTreeSetting
|
from .models import InvenTreeSetting, InvenTreeUserSetting
|
||||||
|
|
||||||
|
|
||||||
class SettingsAdmin(ImportExportModelAdmin):
|
class SettingsAdmin(ImportExportModelAdmin):
|
||||||
@ -13,4 +13,10 @@ class SettingsAdmin(ImportExportModelAdmin):
|
|||||||
list_display = ('key', 'value')
|
list_display = ('key', 'value')
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
list_display = ('key', 'value', 'user', )
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(InvenTreeSetting, SettingsAdmin)
|
admin.site.register(InvenTreeSetting, SettingsAdmin)
|
||||||
|
admin.site.register(InvenTreeUserSetting, UserSettingsAdmin)
|
||||||
|
33
InvenTree/common/migrations/0011_auto_20210722_2114.py
Normal file
33
InvenTree/common/migrations/0011_auto_20210722_2114.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 3.2.4 on 2021-07-22 21:14
|
||||||
|
|
||||||
|
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')),
|
||||||
|
('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={
|
||||||
|
'verbose_name': 'InvenTree User Setting',
|
||||||
|
'verbose_name_plural': 'InvenTree User Settings',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='inventreeusersetting',
|
||||||
|
constraint=models.UniqueConstraint(fields=('key', 'user'), name='unique key and user'),
|
||||||
|
),
|
||||||
|
]
|
@ -11,6 +11,7 @@ import decimal
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.db.utils import IntegrityError, OperationalError
|
from django.db.utils import IntegrityError, OperationalError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@ -26,7 +27,397 @@ import InvenTree.helpers
|
|||||||
import InvenTree.fields
|
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_filters(cls, key, **kwargs):
|
||||||
|
return {'key__iexact': key}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_setting_object(cls, key, **kwargs):
|
||||||
|
"""
|
||||||
|
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(**cls.get_filters(key, **kwargs)).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), **kwargs)
|
||||||
|
|
||||||
|
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, **kwargs):
|
||||||
|
"""
|
||||||
|
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, **kwargs)
|
||||||
|
|
||||||
|
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, change_user, create=True, **kwargs):
|
||||||
|
"""
|
||||||
|
Set the value of a particular setting.
|
||||||
|
If it does not exist, option to create it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: settings key
|
||||||
|
value: New value
|
||||||
|
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 change_user is not None and not change_user.is_staff:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
setting = cls.objects.get(**cls.get_filters(key, **kwargs))
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
|
||||||
|
if create:
|
||||||
|
setting = cls(key=key, **kwargs)
|
||||||
|
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=False, 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, **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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().validate_unique(exclude)
|
||||||
|
|
||||||
|
try:
|
||||||
|
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:
|
||||||
|
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
|
An InvenTreeSetting object is a key:value pair used for storing
|
||||||
single values (e.g. one-off settings values).
|
single values (e.g. one-off settings values).
|
||||||
@ -362,379 +753,144 @@ class InvenTreeSetting(models.Model):
|
|||||||
verbose_name = "InvenTree Setting"
|
verbose_name = "InvenTree Setting"
|
||||||
verbose_name_plural = "InvenTree Settings"
|
verbose_name_plural = "InvenTree Settings"
|
||||||
|
|
||||||
@classmethod
|
key = models.CharField(
|
||||||
def get_setting_name(cls, key):
|
max_length=50,
|
||||||
"""
|
blank=False,
|
||||||
Return the name of a particular setting.
|
unique=True,
|
||||||
|
help_text=_('Settings key (must be unique - case insensitive'),
|
||||||
|
)
|
||||||
|
|
||||||
If it does not exist, return an empty string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = str(key).strip().upper()
|
class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||||
|
"""
|
||||||
|
An InvenTreeSetting object with a usercontext
|
||||||
|
"""
|
||||||
|
|
||||||
if key in cls.GLOBAL_SETTINGS:
|
GLOBAL_SETTINGS = {
|
||||||
setting = cls.GLOBAL_SETTINGS[key]
|
'HOMEPAGE_PART_STARRED': {
|
||||||
return setting.get('name', '')
|
'name': _('Show starred parts'),
|
||||||
else:
|
'description': _('Show starred parts on the homepage'),
|
||||||
return ''
|
'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 unvalidated BOMs'),
|
||||||
|
'description': _('Show BOMs that await validation on the homepage'),
|
||||||
|
'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:
|
||||||
|
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,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('User'),
|
||||||
|
help_text=_('User'),
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_description(cls, key):
|
def get_setting_object(cls, key, user):
|
||||||
"""
|
return super().get_setting_object(key, user=user)
|
||||||
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):
|
def validate_unique(self, exclude=None):
|
||||||
""" Ensure that the key:value pair is unique.
|
return super().validate_unique(exclude=exclude, user=self.user)
|
||||||
In addition to the base validators, this ensures that the 'key'
|
|
||||||
is unique, using a case-insensitive comparison.
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().validate_unique(exclude)
|
@classmethod
|
||||||
|
def get_filters(cls, key, **kwargs):
|
||||||
try:
|
return {'key__iexact': key, 'user__id__iexact': kwargs['user'].id}
|
||||||
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):
|
class PriceBreak(models.Model):
|
||||||
|
@ -45,8 +45,8 @@ class SettingEdit(AjaxUpdateView):
|
|||||||
|
|
||||||
ctx['key'] = setting.key
|
ctx['key'] = setting.key
|
||||||
ctx['value'] = setting.value
|
ctx['value'] = setting.value
|
||||||
ctx['name'] = models.InvenTreeSetting.get_setting_name(setting.key)
|
ctx['name'] = self.model.get_setting_name(setting.key)
|
||||||
ctx['description'] = models.InvenTreeSetting.get_setting_description(setting.key)
|
ctx['description'] = self.model.get_setting_description(setting.key)
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@ -69,12 +69,12 @@ class SettingEdit(AjaxUpdateView):
|
|||||||
self.object.value = str2bool(setting.value)
|
self.object.value = str2bool(setting.value)
|
||||||
form.fields['value'].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:
|
if name:
|
||||||
form.fields['value'].label = name
|
form.fields['value'].label = name
|
||||||
|
|
||||||
description = models.InvenTreeSetting.get_setting_description(setting.key)
|
description = self.model.get_setting_description(setting.key)
|
||||||
|
|
||||||
if description:
|
if description:
|
||||||
form.fields['value'].help_text = description
|
form.fields['value'].help_text = description
|
||||||
@ -111,6 +111,18 @@ class SettingEdit(AjaxUpdateView):
|
|||||||
form.add_error('value', _('Supplied value must be a boolean'))
|
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):
|
class MultiStepFormView(SessionWizardView):
|
||||||
""" Setup basic methods of multi-step form
|
""" Setup basic methods of multi-step form
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ from InvenTree import version, settings
|
|||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
|
||||||
from common.models import InvenTreeSetting, ColorTheme
|
from common.models import InvenTreeSetting, ColorTheme, InvenTreeUserSetting
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
@ -69,6 +69,12 @@ def add(x, y, *args, **kwargs):
|
|||||||
return x + y
|
return x + y
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def to_list(*args):
|
||||||
|
""" Return the input arguments as list """
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def part_allocation_count(build, part, *args, **kwargs):
|
def part_allocation_count(build, part, *args, **kwargs):
|
||||||
""" Return the total number of <part> allocated to <build> """
|
""" Return the total number of <part> allocated to <build> """
|
||||||
@ -182,11 +188,12 @@ def setting_object(key, *args, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Return a setting object speciifed by the given key
|
Return a setting object speciifed by the given key
|
||||||
(Or return None if the setting does not exist)
|
(Or return None if the setting does not exist)
|
||||||
|
if a user-setting was requested return that
|
||||||
"""
|
"""
|
||||||
|
|
||||||
setting = InvenTreeSetting.get_setting_object(key)
|
if 'user' in kwargs:
|
||||||
|
return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'])
|
||||||
return setting
|
return InvenTreeSetting.get_setting_object(key)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
@ -195,6 +202,8 @@ def settings_value(key, *args, **kwargs):
|
|||||||
Return a settings value specified by the given key
|
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)
|
return InvenTreeSetting.get_setting(key)
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,13 +93,26 @@ function addHeaderAction(label, title, icon, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{% if roles.part.view %}
|
{% 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 and True in settings_list_part %}
|
||||||
addHeaderTitle('{% trans "Parts" %}');
|
addHeaderTitle('{% trans "Parts" %}');
|
||||||
|
|
||||||
|
{% if setting_part_starred %}
|
||||||
addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star');
|
addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star');
|
||||||
|
loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", {
|
||||||
|
params: {
|
||||||
|
"starred": true,
|
||||||
|
},
|
||||||
|
name: 'starred_parts',
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if setting_part_latest %}
|
||||||
addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper');
|
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' %}", {
|
loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
|
||||||
params: {
|
params: {
|
||||||
ordering: "-creation_date",
|
ordering: "-creation_date",
|
||||||
@ -107,30 +120,37 @@ loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
|
|||||||
},
|
},
|
||||||
name: 'latest_parts',
|
name: 'latest_parts',
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", {
|
{% if setting_bom_validation %}
|
||||||
params: {
|
addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle');
|
||||||
"starred": true,
|
|
||||||
},
|
|
||||||
name: 'starred_parts',
|
|
||||||
});
|
|
||||||
|
|
||||||
loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", {
|
loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", {
|
||||||
params: {
|
params: {
|
||||||
"bom_valid": false,
|
"bom_valid": false,
|
||||||
},
|
},
|
||||||
name: 'bom_invalid_parts',
|
name: 'bom_invalid_parts',
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if roles.stock.view %}
|
{% settings_value 'HOMEPAGE_STOCK_RECENT' user=request.user as setting_stock_recent %}
|
||||||
addHeaderTitle('{% trans "Stock" %}');
|
{% settings_value 'HOMEPAGE_STOCK_LOW' user=request.user as setting_stock_low %}
|
||||||
addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock');
|
{% settings_value 'HOMEPAGE_STOCK_DEPLETED' user=request.user as setting_stock_depleted %}
|
||||||
addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart');
|
{% settings_value 'HOMEPAGE_STOCK_NEEDED' user=request.user as setting_stock_needed %}
|
||||||
addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times');
|
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
|
||||||
addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn');
|
{% 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 and True in settings_list_stock %}
|
||||||
|
addHeaderTitle('{% trans "Stock" %}');
|
||||||
|
|
||||||
|
{% if setting_stock_recent %}
|
||||||
|
addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock');
|
||||||
loadStockTable($('#table-recently-updated-stock'), {
|
loadStockTable($('#table-recently-updated-stock'), {
|
||||||
params: {
|
params: {
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
@ -140,12 +160,43 @@ loadStockTable($('#table-recently-updated-stock'), {
|
|||||||
name: 'recently-updated-stock',
|
name: 'recently-updated-stock',
|
||||||
grouping: false,
|
grouping: false,
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
{% 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 %}
|
{% if expiry %}
|
||||||
addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times');
|
|
||||||
addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch');
|
|
||||||
|
|
||||||
|
{% if setting_stock_expired %}
|
||||||
|
addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times');
|
||||||
loadStockTable($("#table-expired-stock"), {
|
loadStockTable($("#table-expired-stock"), {
|
||||||
params: {
|
params: {
|
||||||
expired: true,
|
expired: true,
|
||||||
@ -153,7 +204,10 @@ loadStockTable($("#table-expired-stock"), {
|
|||||||
part_detail: true,
|
part_detail: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if setting_stock_stale %}
|
||||||
|
addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch');
|
||||||
loadStockTable($("#table-stale-stock"), {
|
loadStockTable($("#table-stale-stock"), {
|
||||||
params: {
|
params: {
|
||||||
stale: true,
|
stale: true,
|
||||||
@ -164,34 +218,18 @@ loadStockTable($("#table-stale-stock"), {
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", {
|
{% endif %}
|
||||||
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 %}
|
{% settings_value 'HOMEPAGE_BUILD_PENDING' user=request.user as setting_build_pending %}
|
||||||
addHeaderTitle('{% trans "Build Orders" %}');
|
{% settings_value 'HOMEPAGE_BUILD_OVERDUE' user=request.user as setting_build_overdue %}
|
||||||
addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs');
|
{% to_list setting_build_pending setting_build_overdue as settings_list_build %}
|
||||||
addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times');
|
|
||||||
|
|
||||||
|
{% if roles.build.view and True in settings_list_build %}
|
||||||
|
addHeaderTitle('{% trans "Build Orders" %}');
|
||||||
|
|
||||||
|
{% if setting_build_pending %}
|
||||||
|
addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs');
|
||||||
loadBuildTable("#table-build-pending", {
|
loadBuildTable("#table-build-pending", {
|
||||||
url: "{% url 'api-build-list' %}",
|
url: "{% url 'api-build-list' %}",
|
||||||
params: {
|
params: {
|
||||||
@ -199,7 +237,10 @@ loadBuildTable("#table-build-pending", {
|
|||||||
},
|
},
|
||||||
disableFilters: true,
|
disableFilters: true,
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if setting_build_overdue %}
|
||||||
|
addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times');
|
||||||
loadBuildTable("#table-build-overdue", {
|
loadBuildTable("#table-build-overdue", {
|
||||||
url: "{% url 'api-build-list' %}",
|
url: "{% url 'api-build-list' %}",
|
||||||
params: {
|
params: {
|
||||||
@ -209,11 +250,17 @@ loadBuildTable("#table-build-overdue", {
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if roles.purchase_order.view %}
|
{% endif %}
|
||||||
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 %}
|
||||||
|
{% 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 and True in settings_list_po %}
|
||||||
|
addHeaderTitle('{% trans "Purchase Orders" %}');
|
||||||
|
|
||||||
|
{% if setting_po_outstanding %}
|
||||||
|
addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt');
|
||||||
loadPurchaseOrderTable("#table-po-outstanding", {
|
loadPurchaseOrderTable("#table-po-outstanding", {
|
||||||
url: "{% url 'api-po-list' %}",
|
url: "{% url 'api-po-list' %}",
|
||||||
params: {
|
params: {
|
||||||
@ -221,7 +268,10 @@ loadPurchaseOrderTable("#table-po-outstanding", {
|
|||||||
outstanding: true,
|
outstanding: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if setting_po_overdue %}
|
||||||
|
addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times');
|
||||||
loadPurchaseOrderTable("#table-po-overdue", {
|
loadPurchaseOrderTable("#table-po-overdue", {
|
||||||
url: "{% url 'api-po-list' %}",
|
url: "{% url 'api-po-list' %}",
|
||||||
params: {
|
params: {
|
||||||
@ -229,14 +279,19 @@ loadPurchaseOrderTable("#table-po-overdue", {
|
|||||||
overdue: true,
|
overdue: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if roles.sales_order.view %}
|
{% settings_value 'HOMEPAGE_SO_OUTSTANDING' user=request.user as setting_so_outstanding %}
|
||||||
addHeaderTitle('{% trans "Sales Orders" %}');
|
{% settings_value 'HOMEPAGE_SO_OVERDUE' user=request.user as setting_so_overdue %}
|
||||||
addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt');
|
{% to_list setting_so_outstanding setting_so_overdue as settings_list_so %}
|
||||||
addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times');
|
|
||||||
|
|
||||||
|
{% if roles.sales_order.view and True in settings_list_so %}
|
||||||
|
addHeaderTitle('{% trans "Sales Orders" %}');
|
||||||
|
|
||||||
|
{% if setting_so_outstanding %}
|
||||||
|
addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt');
|
||||||
loadSalesOrderTable("#table-so-outstanding", {
|
loadSalesOrderTable("#table-so-outstanding", {
|
||||||
url: "{% url 'api-so-list' %}",
|
url: "{% url 'api-so-list' %}",
|
||||||
params: {
|
params: {
|
||||||
@ -244,7 +299,10 @@ loadSalesOrderTable("#table-so-outstanding", {
|
|||||||
outstanding: true,
|
outstanding: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if setting_so_overdue %}
|
||||||
|
addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times');
|
||||||
loadSalesOrderTable("#table-so-overdue", {
|
loadSalesOrderTable("#table-so-overdue", {
|
||||||
url: "{% url 'api-so-list' %}",
|
url: "{% url 'api-so-list' %}",
|
||||||
params: {
|
params: {
|
||||||
@ -252,6 +310,7 @@ loadSalesOrderTable("#table-so-overdue", {
|
|||||||
customer_detail: true,
|
customer_detail: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -62,6 +62,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% 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 %}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if icon %}
|
{% if icon %}
|
||||||
@ -28,7 +33,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class='btn-group float-right'>
|
<div class='btn-group float-right'>
|
||||||
<button class='btn btn-default btn-glyph btn-edit-setting' pk='{{ setting.pk }}' setting='{{ key }}' title='{% trans "Edit setting" %}'>
|
<button class='btn btn-default btn-glyph btn-edit-setting' pk='{{ setting.pk }}' setting='{{ key }}' title='{% trans "Edit setting" %}' {% if user_setting %}user='{{request.user.id}}'{% endif %}>
|
||||||
<span class='fas fa-edit icon-green'></span>
|
<span class='fas fa-edit icon-green'></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,9 +45,14 @@
|
|||||||
$('table').find('.btn-edit-setting').click(function() {
|
$('table').find('.btn-edit-setting').click(function() {
|
||||||
var setting = $(this).attr('setting');
|
var setting = $(this).attr('setting');
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
var url = `/settings/${pk}/edit/`;
|
||||||
|
|
||||||
|
if ($(this).attr('user')){
|
||||||
|
url += `user/`;
|
||||||
|
}
|
||||||
|
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
`/settings/${pk}/edit/`,
|
url,
|
||||||
{
|
{
|
||||||
reload: true,
|
reload: true,
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,9 @@
|
|||||||
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
|
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
|
||||||
<a href="{% url 'settings-appearance' %}"><span class='fas fa-fill'></span> {% trans "Appearance" %}</a>
|
<a href="{% url 'settings-appearance' %}"><span class='fas fa-fill'></span> {% trans "Appearance" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li{% ifequal tab 'user_settings' %} class='active'{% endifequal %}>
|
||||||
|
<a href="{% url 'settings-user-settings' %}"><span class='fas fa-cog'></span> {% trans "User Settings" %}</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<h4><span class='fas fa-cogs'></span> {% trans "InvenTree Settings" %}</h4>
|
<h4><span class='fas fa-cogs'></span> {% trans "InvenTree Settings" %}</h4>
|
||||||
|
41
InvenTree/templates/InvenTree/settings/user_settings.html
Normal file
41
InvenTree/templates/InvenTree/settings/user_settings.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
{% include "InvenTree/settings/header.html" %}
|
||||||
|
<tbody>
|
||||||
|
{% 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 %}
|
||||||
|
<tr><td colspan='5'></td></tr>
|
||||||
|
{% 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 %}
|
||||||
|
<tr><td colspan='5'></td></tr>
|
||||||
|
{% 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 %}
|
||||||
|
<tr><td colspan='5'></td></tr>
|
||||||
|
{% 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 %}
|
||||||
|
<tr><td colspan='5'></td></tr>
|
||||||
|
{% 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 %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -141,6 +141,7 @@ class RuleSet(models.Model):
|
|||||||
# Models which currently do not require permissions
|
# Models which currently do not require permissions
|
||||||
'common_colortheme',
|
'common_colortheme',
|
||||||
'common_inventreesetting',
|
'common_inventreesetting',
|
||||||
|
'common_inventreeusersetting',
|
||||||
'company_contact',
|
'company_contact',
|
||||||
'users_owner',
|
'users_owner',
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user