diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index da96c7ee79..921a45c112 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -120,6 +120,19 @@ def str2bool(text, test=True): return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ] +def is_bool(text): + """ + Determine if a string value 'looks' like a boolean. + """ + + if str2bool(text, True): + return True + elif str2bool(text, False): + return True + else: + return False + + def isNull(text): """ Test if a string 'looks' like a null value. diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 7015e00d14..7738e84111 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -39,6 +39,8 @@ from .views import IndexView, SearchView, DatabaseStatsView from .views import SettingsView, EditUserView, SetPasswordView, ColorThemeSelectView from .views import DynamicJsView +from common.views import SettingEdit + from .api import InfoView from .api import ActionPluginView @@ -71,6 +73,7 @@ settings_urls = [ url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'), url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'), + url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'), url(r'^currency/?', SettingsView.as_view(template_name='InvenTree/settings/currency.html'), name='settings-currency'), url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'), url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'), @@ -78,6 +81,8 @@ settings_urls = [ url(r'^purchase-order/?', SettingsView.as_view(template_name='InvenTree/settings/po.html'), name='settings-po'), url(r'^sales-order/?', SettingsView.as_view(template_name='InvenTree/settings/so.html'), name='settings-so'), + url(r'^(?P\d+)/edit/?', SettingEdit.as_view(), name='setting-edit'), + # Catch any other urls url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'), ] diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 83a1eb05af..f73be6bd4e 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -128,9 +128,13 @@ class InvenTreeRoleMixin(PermissionRequiredMixin): def has_permission(self): """ - Determine if the current user + Determine if the current user has specified permissions """ + if self.permission_required: + # Ignore role-based permissions + return super().has_permission() + roles_required = [] if type(self.role_required) is str: diff --git a/InvenTree/common/apps.py b/InvenTree/common/apps.py index afdf845f15..06b825c574 100644 --- a/InvenTree/common/apps.py +++ b/InvenTree/common/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig -from django.db.utils import OperationalError, ProgrammingError +from django.db.utils import OperationalError, ProgrammingError, IntegrityError class CommonConfig(AppConfig): @@ -32,7 +32,7 @@ class CommonConfig(AppConfig): return # Default instance name - instance_name = 'InvenTree Server' + instance_name = InvenTreeSetting.get_default_value('INVENTREE_INSTANCE') # Use the old name if it exists if InvenTreeSetting.objects.filter(key='InstanceName').exists(): @@ -48,7 +48,7 @@ class CommonConfig(AppConfig): value=instance_name ) - except (OperationalError, ProgrammingError): + except (OperationalError, ProgrammingError, IntegrityError): # Migrations have not yet been applied - table does not exist pass @@ -59,12 +59,12 @@ class CommonConfig(AppConfig): from .models import InvenTreeSetting - for key in InvenTreeSetting.DEFAULT_VALUES.keys(): + for key in InvenTreeSetting.GLOBAL_SETTINGS.keys(): try: settings = InvenTreeSetting.objects.filter(key__iexact=key) if settings.count() == 0: - value = InvenTreeSetting.DEFAULT_VALUES[key] + value = InvenTreeSetting.get_default_value(key) print(f"Creating default setting for {key} -> '{value}'") @@ -87,6 +87,6 @@ class CommonConfig(AppConfig): setting.key = key setting.save() - except (OperationalError, ProgrammingError): + except (OperationalError, ProgrammingError, IntegrityError): # Table might not yet exist pass diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index 53493faff0..ba6289221e 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -33,6 +33,5 @@ class SettingEditForm(HelperForm): model = InvenTreeSetting fields = [ - 'key', 'value' ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 31f8e12260..0d3afbe226 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -11,10 +11,12 @@ import decimal from django.db import models from django.conf import settings + from django.utils.translation import ugettext as _ from django.core.validators import MinValueValidator, MaxValueValidator from django.core.exceptions import ValidationError +import InvenTree.helpers import InvenTree.fields @@ -27,34 +29,205 @@ class InvenTreeSetting(models.Model): even if that key does not exist. """ - # Dict of default values for various internal settings - DEFAULT_VALUES = { - # Global inventree settings - 'INVENTREE_INSTANCE': 'InvenTree Server', + """ + Dict of all global settings values: - # Part settings - 'PART_IPN_REGEX': '', - 'PART_COPY_BOM': True, - 'PART_COPY_PARAMETERS': True, - 'PART_COPY_TESTS': True, + The key of each item is the name of the value as it appears in the database. - # Stock settings + Each global setting has the following parameters: + + - name: Translatable string name of the setting (required) + - description: Translatable string description of the setting (required) + - default: Default value (optional) + - units: Units of the particular setting (optional) + - validator: Validation function for the setting (optional) - # Build Order settings - 'BUILDORDER_REFERENCE_PREFIX': 'BO', - 'BUILDORDER_REFERENCE_REGEX': '', + The keys must be upper-case + """ - # Purchase Order Settings - 'PURCHASEORDER_REFERENCE_PREFIX': 'PO', + GLOBAL_SETTINGS = { - # Sales Order Settings - 'SALESORDER_REFERENCE_PREFIX': 'SO', + 'INVENTREE_INSTANCE': { + 'name': _('InvenTree Instance Name'), + 'default': 'InvenTree server', + 'description': _('String descriptor for the server instance'), + }, + + 'INVENTREE_COMPANY_NAME': { + 'name': _('Company name'), + 'description': _('Internal company name'), + 'default': 'My company name', + }, + + 'PART_IPN_REGEX': { + 'name': _('IPN Regex'), + 'description': _('Regular expression pattern for matching Part IPN') + }, + + 'PART_COPY_BOM': { + 'name': _('Copy Part BOM Data'), + 'description': _('Copy BOM data by default when duplicating a part'), + 'default': True, + 'validator': bool, + }, + + 'PART_COPY_PARAMETERS': { + 'name': _('Copy Part Parameter Data'), + 'description': _('Copy parameter data by default when duplicating a part'), + 'default': True, + 'validator': bool, + }, + + 'PART_COPY_TESTS': { + 'name': _('Copy Part Test Data'), + 'description': _('Copy test data by default when duplicating a part'), + 'default': True, + 'validator': bool + }, + + 'BUILDORDER_REFERENCE_PREFIX': { + 'name': _('Build Order Reference Prefix'), + 'description': _('Prefix value for build order reference'), + 'default': 'BO', + }, + + 'BUILDORDER_REFERENCE_REGEX': { + 'name': _('Build Order Reference Regex'), + 'description': _('Regular expression pattern for matching build order reference') + }, + + 'SALESORDER_REFERENCE_PREFIX': { + 'name': _('Sales Order Reference Prefix'), + 'description': _('Prefix value for sales order reference'), + }, + + 'PURCHASEORDER_REFERENCE_PREFIX': { + 'name': _('Purchase Order Reference Prefix'), + 'description': _('Prefix value for purchase order reference'), + }, } class Meta: 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_default_value(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_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 (InvenTreeSetting.DoesNotExist): + # Create the setting if it does not exist + setting = InvenTreeSetting.create( + key=key, + value=InvenTreeSetting.get_default_value(key) + ) + + 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): """ @@ -64,16 +237,13 @@ class InvenTreeSetting(models.Model): # If no backup value is specified, atttempt to retrieve a "default" value if backup_value is None: - backup_value = InvenTreeSetting.DEFAULT_VALUES.get(key, None) + backup_value = cls.get_default_value(key) - try: - settings = InvenTreeSetting.objects.filter(key__iexact=key) + setting = InvenTreeSetting.get_setting_object(key) - if len(settings) > 0: - return settings[0].value - else: - return backup_value - except InvenTreeSetting.DoesNotExist: + if setting: + return setting.value + else: return backup_value @classmethod @@ -108,6 +278,59 @@ class InvenTreeSetting(models.Model): 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 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 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 + + # 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) + + return + + # Check if a 'type' has been specified for this value + if type(validator) == type: + + if validator == bool: + # Value must "look like" a boolean value + if InvenTree.helpers.is_bool(self.value): + # Coerce into either "True" or "False" + self.value = str(InvenTree.helpers.str2bool(self.value)) + else: + raise ValidationError({ + 'value': _('Value must be a boolean 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' @@ -123,6 +346,24 @@ class InvenTreeSetting(models.Model): except InvenTreeSetting.DoesNotExist: pass + def is_bool(self): + """ + Check if this setting is required to be a boolean value + """ + + validator = InvenTreeSetting.get_setting_validator(self.key) + + return validator == bool + + 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) + class Currency(models.Model): """ diff --git a/InvenTree/common/templates/common/edit_setting.html b/InvenTree/common/templates/common/edit_setting.html new file mode 100644 index 0000000000..c74ed7d591 --- /dev/null +++ b/InvenTree/common/templates/common/edit_setting.html @@ -0,0 +1,14 @@ +{% extends "modal_form.html" %} +{% load i18n %} + +{% block pre_form_content %} + +{{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index a5d32dc3d6..e0b6812f40 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -35,14 +35,37 @@ class SettingsTest(TestCase): self.client.login(username='username', password='password') + def test_required_values(self): + """ + - Ensure that every global setting has a name. + - Ensure that every global setting has a description. + """ + + for key in InvenTreeSetting.GLOBAL_SETTINGS.keys(): + + setting = InvenTreeSetting.GLOBAL_SETTINGS[key] + + name = setting.get('name', None) + + if name is None: + raise ValueError(f'Missing GLOBAL_SETTING name for {key}') + + description = setting.get('description', None) + + if description is None: + raise ValueError(f'Missing GLOBAL_SETTING description for {key}') + + if not key == key.upper(): + raise ValueError(f"GLOBAL_SETTINGS key '{key}' is not uppercase") + def test_defaults(self): """ Populate the settings with default values """ - for key in InvenTreeSetting.DEFAULT_VALUES.keys(): + for key in InvenTreeSetting.GLOBAL_SETTINGS.keys(): - value = InvenTreeSetting.DEFAULT_VALUES[key] + value = InvenTreeSetting.get_default_value(key) InvenTreeSetting.set_setting(key, value, self.user) diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index a205b8b915..cfcee8bfa9 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -6,8 +6,10 @@ Django views for interacting with common models from __future__ import unicode_literals from django.utils.translation import ugettext as _ +from django.forms import CheckboxInput from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView +from InvenTree.helpers import str2bool from . import models from . import forms @@ -46,3 +48,47 @@ class SettingEdit(AjaxUpdateView): model = models.InvenTreeSetting ajax_form_title = _('Change Setting') form_class = forms.SettingEditForm + ajax_template_name = "common/edit_setting.html" + + def get_context_data(self, **kwargs): + """ + Add extra context information about the particular setting object. + """ + + ctx = super().get_context_data(**kwargs) + + setting = self.get_object() + + 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) + + return ctx + + def get_form(self): + """ + Override default get_form behaviour + """ + + form = super().get_form() + + setting = self.get_object() + + if setting.is_bool(): + form.fields['value'].widget = CheckboxInput() + + self.object.value = str2bool(setting.value) + form.fields['value'].value = str2bool(setting.value) + + name = models.InvenTreeSetting.get_setting_name(setting.key) + + if name: + form.fields['value'].label = name + + description = models.InvenTreeSetting.get_setting_description(setting.key) + + if description: + form.fields['value'].help_text = description + + return form diff --git a/InvenTree/company/migrations/0024_unique_name_email_constraint.py b/InvenTree/company/migrations/0024_unique_name_email_constraint.py new file mode 100644 index 0000000000..3a8781f98d --- /dev/null +++ b/InvenTree/company/migrations/0024_unique_name_email_constraint.py @@ -0,0 +1,38 @@ +from django.db import migrations, models + + +def make_empty_email_field_null(apps, schema_editor): + Company = apps.get_model('company', 'Company') + for company in Company.objects.all(): + if company.email == '': + company.email = None + company.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0023_auto_20200808_0715'), + ] + + operations = [ + # Allow email field to be NULL + migrations.AlterField( + model_name='company', + name='email', + field=models.EmailField(blank=True, help_text='Contact email address', max_length=254, null=True, unique=False, verbose_name='Email'), + ), + # Convert empty email string to NULL + migrations.RunPython(make_empty_email_field_null), + # Remove unique constraint on name field + migrations.AlterField( + model_name='company', + name='name', + field=models.CharField(help_text='Company name', max_length=100, verbose_name='Company name'), + ), + # Add unique constraint on name/email pair + migrations.AddConstraint( + model_name='company', + constraint=models.UniqueConstraint(fields=('name', 'email'), name='unique_name_email_pair'), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index f1e249fd58..b9fed2ee7b 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -12,7 +12,7 @@ import math from django.utils.translation import gettext_lazy as _ from django.core.validators import MinValueValidator from django.db import models -from django.db.models import Sum, Q +from django.db.models import Sum, Q, UniqueConstraint from django.apps import apps from django.urls import reverse @@ -81,8 +81,11 @@ class Company(models.Model): class Meta: ordering = ['name', ] + constraints = [ + UniqueConstraint(fields=['name', 'email'], name='unique_name_email_pair') + ] - name = models.CharField(max_length=100, blank=False, unique=True, + name = models.CharField(max_length=100, blank=False, help_text=_('Company name'), verbose_name=_('Company name')) @@ -98,7 +101,8 @@ class Company(models.Model): verbose_name=_('Phone number'), blank=True, help_text=_('Contact phone number')) - email = models.EmailField(blank=True, verbose_name=_('Email'), help_text=_('Contact email address')) + email = models.EmailField(blank=True, null=True, + verbose_name=_('Email'), help_text=_('Contact email address')) contact = models.CharField(max_length=100, verbose_name=_('Contact'), diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index f20107277d..cdca22fa36 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -23,23 +23,27 @@ InvenTree | {% trans "Company" %} - {{ company.name }}

{{ company.name }} - {% if user.is_staff and roles.company.change %} + {% if user.is_staff and perms.company.change_company %} {% endif %}

{{ company.description }}

- {% if company.is_supplier %} + {% if company.is_supplier and roles.purchase_order.add %} {% endif %} + {% if perms.company.change_company %} + {% endif %} + {% if perms.company.delete_company %} + {% endif %}
{% endblock %} diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index e07304dd13..463bf5814d 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -9,17 +9,25 @@
+{% if roles.purchase_order.change %}
+ {% if roles.purchase_order.add %} + {% endif %}
+{% endif %}
diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 253085568e..f110950c0b 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -12,12 +12,13 @@ InvenTree | {% trans "Supplier List" %}

{{ title }}


+{% if pagetype == 'manufacturers' and roles.purchase_order.add or pagetype == 'suppliers' and roles.purchase_order.add or pagetype == 'customers' and roles.sales_order.add %}
- +{% endif %}
diff --git a/InvenTree/company/templates/company/purchase_orders.html b/InvenTree/company/templates/company/purchase_orders.html index bab5cd4bce..a0ef1612fa 100644 --- a/InvenTree/company/templates/company/purchase_orders.html +++ b/InvenTree/company/templates/company/purchase_orders.html @@ -9,6 +9,7 @@

{% trans "Purchase Orders" %}


+{% if roles.purchase_order.add %}
@@ -17,6 +18,7 @@
+{% endif %}
diff --git a/InvenTree/company/templates/company/sales_orders.html b/InvenTree/company/templates/company/sales_orders.html index 0b64bed2f5..03c64d5b88 100644 --- a/InvenTree/company/templates/company/sales_orders.html +++ b/InvenTree/company/templates/company/sales_orders.html @@ -9,6 +9,7 @@

{% trans "Sales Orders" %}


+{% if roles.sales_order.add %}
@@ -17,6 +18,7 @@
+{% endif %}
diff --git a/InvenTree/company/templates/company/supplier_part_base.html b/InvenTree/company/templates/company/supplier_part_base.html index ca09caee93..7476a7c606 100644 --- a/InvenTree/company/templates/company/supplier_part_base.html +++ b/InvenTree/company/templates/company/supplier_part_base.html @@ -18,19 +18,27 @@ src="{% static 'img/blank_image.png' %}" {% block page_data %}

{% trans "Supplier Part" %}

{{ part.supplier.name }} - {{ part.SKU }}

+ +{% if roles.purchase_order.change %}
+ {% if roles.purchase_order.add %} + {% endif %} + {% if roles.purchase_order.delete %} + {% endif %}
+{% endif %} + {% endblock %} {% block page_details %} diff --git a/InvenTree/company/templates/company/supplier_part_orders.html b/InvenTree/company/templates/company/supplier_part_orders.html index 5c2ea6d1d4..29eb8ee874 100644 --- a/InvenTree/company/templates/company/supplier_part_orders.html +++ b/InvenTree/company/templates/company/supplier_part_orders.html @@ -10,11 +10,13 @@
+{% if roles.purchase_order.add %}
+{% endif %}
diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html index f9f5063190..97022024f5 100644 --- a/InvenTree/company/templates/company/supplier_part_pricing.html +++ b/InvenTree/company/templates/company/supplier_part_pricing.html @@ -11,9 +11,11 @@
+{% if roles.purchase_order.add %}
+{% endif %}
diff --git a/InvenTree/company/test_views.py b/InvenTree/company/test_views.py index b9bb69e503..d895c18957 100644 --- a/InvenTree/company/test_views.py +++ b/InvenTree/company/test_views.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group from .models import SupplierPart @@ -25,7 +26,24 @@ class CompanyViewTest(TestCase): # Create a user User = get_user_model() - User.objects.create_user('username', 'user@email.com', 'password') + self.user = User.objects.create_user( + username='username', + email='user@email.com', + password='password' + ) + + # Put the user into a group with the correct permissions + group = Group.objects.create(name='mygroup') + self.user.groups.add(group) + + # Give the group *all* the permissions! + for rule in group.rule_sets.all(): + rule.can_view = True + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() self.client.login(username='username', password='password') diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 9ef6adea0e..dce341d184 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -14,6 +14,7 @@ from django.forms import HiddenInput from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.helpers import str2bool +from InvenTree.views import InvenTreeRoleMixin from common.models import Currency @@ -29,7 +30,7 @@ from .forms import EditSupplierPartForm from .forms import EditPriceBreakForm -class CompanyIndex(ListView): +class CompanyIndex(InvenTreeRoleMixin, ListView): """ View for displaying list of companies """ @@ -37,6 +38,7 @@ class CompanyIndex(ListView): template_name = 'company/index.html' context_object_name = 'companies' paginate_by = 50 + permission_required = 'company.view_company' def get_context_data(self, **kwargs): @@ -116,8 +118,8 @@ class CompanyNotes(UpdateView): context_object_name = 'company' template_name = 'company/notes.html' model = Company - fields = ['notes'] + permission_required = 'company.view_company' def get_success_url(self): return reverse('company-notes', kwargs={'pk': self.get_object().id}) @@ -137,6 +139,7 @@ class CompanyDetail(DetailView): template_name = 'company/detail.html' queryset = Company.objects.all() model = Company + permission_required = 'company.view_company' def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) @@ -150,6 +153,7 @@ class CompanyImage(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Update Company Image') form_class = CompanyImageForm + permission_required = 'company.change_company' def get_data(self): return { @@ -164,6 +168,7 @@ class CompanyEdit(AjaxUpdateView): context_object_name = 'company' ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit Company') + permission_required = 'company.change_company' def get_data(self): return { @@ -177,6 +182,7 @@ class CompanyCreate(AjaxCreateView): context_object_name = 'company' form_class = EditCompanyForm ajax_template_name = 'modal_form.html' + permission_required = 'company.add_company' def get_form_title(self): @@ -230,6 +236,7 @@ class CompanyDelete(AjaxDeleteView): ajax_template_name = 'company/delete.html' ajax_form_title = _('Delete Company') context_object_name = 'company' + permission_required = 'company.delete_company' def get_data(self): return { @@ -243,6 +250,7 @@ class SupplierPartDetail(DetailView): template_name = 'company/supplier_part_detail.html' context_object_name = 'part' queryset = SupplierPart.objects.all() + permission_required = 'purchase_order.view' def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) @@ -258,6 +266,7 @@ class SupplierPartEdit(AjaxUpdateView): form_class = EditSupplierPartForm ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit Supplier Part') + role_required = 'purchase_order.change' class SupplierPartCreate(AjaxCreateView): @@ -268,6 +277,7 @@ class SupplierPartCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Create new Supplier Part') context_object_name = 'part' + role_required = 'purchase_order.add' def get_form(self): """ Create Form instance to create a new SupplierPart object. @@ -327,6 +337,7 @@ class SupplierPartDelete(AjaxDeleteView): success_url = '/supplier/' ajax_template_name = 'company/partdelete.html' ajax_form_title = _('Delete Supplier Part') + role_required = 'purchase_order.delete' parts = [] @@ -398,6 +409,7 @@ class PriceBreakCreate(AjaxCreateView): form_class = EditPriceBreakForm ajax_form_title = _('Add Price Break') ajax_template_name = 'modal_form.html' + role_required = 'purchase_order.add' def get_data(self): return { @@ -440,6 +452,7 @@ class PriceBreakEdit(AjaxUpdateView): form_class = EditPriceBreakForm ajax_form_title = _('Edit Price Break') ajax_template_name = 'modal_form.html' + role_required = 'purchase_order.change' def get_form(self): @@ -455,3 +468,4 @@ class PriceBreakDelete(AjaxDeleteView): model = SupplierPriceBreak ajax_form_title = _("Delete Price Break") ajax_template_name = 'modal_delete_form.html' + role_required = 'purchase_order.delete' diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index b6cc761cc7..d21a4e950e 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -13,9 +13,11 @@

{% trans "Sales Order Items" %}

+{% if roles.sales_order.change %}
+{% endif %} diff --git a/InvenTree/part/templates/part/notes.html b/InvenTree/part/templates/part/notes.html index 3f833325cd..1c46f53bcd 100644 --- a/InvenTree/part/templates/part/notes.html +++ b/InvenTree/part/templates/part/notes.html @@ -37,7 +37,9 @@
+ {% if part.notes %} {{ part.notes | markdownify }} + {% endif %}
diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 9677e3b228..63276fc43c 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -5,7 +5,8 @@ import os from django import template from InvenTree import version, settings -from InvenTree.helpers import decimal2string + +import InvenTree.helpers from common.models import InvenTreeSetting, ColorTheme @@ -29,7 +30,14 @@ def define(value, *args, **kwargs): def decimal(x, *args, **kwargs): """ Simplified rendering of a decimal number """ - return decimal2string(x) + return InvenTree.helpers.decimal2string(x) + + +@register.simple_tag() +def str2bool(x, *args, **kwargs): + """ Convert a string to a boolean value """ + + return InvenTree.helpers.str2bool(x) @register.simple_tag() @@ -41,7 +49,7 @@ def inrange(n, *args, **kwargs): @register.simple_tag() def multiply(x, y, *args, **kwargs): """ Multiply two numbers together """ - return decimal2string(x * y) + return InvenTree.helpers.decimal2string(x * y) @register.simple_tag() @@ -54,7 +62,7 @@ def add(x, y, *args, **kwargs): def part_allocation_count(build, part, *args, **kwargs): """ Return the total number of allocated to """ - return decimal2string(build.getAllocatedQuantity(part)) + return InvenTree.helpers.decimal2string(build.getAllocatedQuantity(part)) @register.simple_tag() @@ -100,8 +108,15 @@ def inventree_docs_url(*args, **kwargs): @register.simple_tag() -def inventree_setting(key, *args, **kwargs): - return InvenTreeSetting.get_setting(key, backup_value=kwargs.get('backup', None)) +def setting_object(key, *args, **kwargs): + """ + Return a setting object speciifed by the given key + (Or return None if the setting does not exist) + """ + + setting = InvenTreeSetting.get_setting_object(key) + + return setting @register.simple_tag() diff --git a/InvenTree/templates/InvenTree/settings/build.html b/InvenTree/templates/InvenTree/settings/build.html index 6d19e21f1a..781402795b 100644 --- a/InvenTree/templates/InvenTree/settings/build.html +++ b/InvenTree/templates/InvenTree/settings/build.html @@ -15,17 +15,8 @@
- - - - - - - - - - - + {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PREFIX" %} + {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_REGEX" %}
{% trans "Reference Prefix" %}{% inventree_setting 'BUILDORDER_REFERENCE_PREFIX' backup='BO' %}{% trans "Prefix for Build Order reference" %}
{% trans "Reference Regex" %}{% inventree_setting 'BUILDORDER_REFERENCE_REGEX' %}{% trans "Regex validator for Build Order reference" %}
diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html new file mode 100644 index 0000000000..7dcdd54bea --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/global.html @@ -0,0 +1,23 @@ +{% extends "InvenTree/settings/settings.html" %} +{% load i18n %} +{% load inventree_extras %} + +{% block tabs %} +{% include "InvenTree/settings/tabs.html" with tab='global' %} +{% endblock %} + +{% block subtitle %} +{% trans "Global InvenTree Settings" %} +{% endblock %} + +{% block settings %} + + + + + {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" %} + {% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" %} + +
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index 7c028ca6d6..cac04a60ff 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -11,6 +11,16 @@ {% block settings %} + + + + {% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %} + {% include "InvenTree/settings/setting.html" with key="PART_COPY_BOM" %} + {% include "InvenTree/settings/setting.html" with key="PART_COPY_PARAMETERS" %} + {% include "InvenTree/settings/setting.html" with key="PART_COPY_TESTS" %} + +
+

{% trans "Part Parameter Templates" %}

@@ -53,7 +63,7 @@ var bEdit = ""; var bDel = ""; - var html = "
" + bEdit + bDel + "
"; + var html = "
" + bEdit + bDel + "
"; return html; } diff --git a/InvenTree/templates/InvenTree/settings/po.html b/InvenTree/templates/InvenTree/settings/po.html index 7d32611404..a709d40dd3 100644 --- a/InvenTree/templates/InvenTree/settings/po.html +++ b/InvenTree/templates/InvenTree/settings/po.html @@ -10,4 +10,10 @@ {% endblock %} {% block settings %} + + + + {% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_REFERENCE_PREFIX" %} + +
{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html new file mode 100644 index 0000000000..ffbb78cbbc --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/setting.html @@ -0,0 +1,29 @@ +{% load inventree_extras %} +{% load i18n %} + +{% setting_object key as setting %} + + {{ setting.name }} + + {% if setting.is_bool %} +
+ +
+ {% else %} + {% if setting.value %} + {{ setting.value }}{{ setting.units }} + {% else %} + {% trans "No value set" %} + {% endif %} + {% endif %} + + {{ setting.description }} + + +
+ +
+ + \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index 90adfd5fed..fe9fd00e53 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -37,3 +37,20 @@ InvenTree | {% trans "Settings" %} {% block js_load %} {{ block.super }} {% endblock %} + +{% block js_ready %} +{{ block.super }} + +$('table').find('.btn-edit-setting').click(function() { + var setting = $(this).attr('setting'); + var pk = $(this).attr('pk'); + + launchModalForm( + `/settings/${pk}/edit/`, + { + reload: true, + } + ); +}); + +{% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/so.html b/InvenTree/templates/InvenTree/settings/so.html index e66fd85148..368374532f 100644 --- a/InvenTree/templates/InvenTree/settings/so.html +++ b/InvenTree/templates/InvenTree/settings/so.html @@ -10,4 +10,12 @@ {% endblock %} {% block settings %} + + + + + {% include "InvenTree/settings/setting.html" with key="SALESORDER_REFERENCE_PREFIX" %} + +
+ {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index ee402feb4f..d104908c49 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -9,8 +9,12 @@ {% trans "Theme" %} +{% if user.is_staff %}

{% trans "InvenTree Settings" %}

\ No newline at end of file + +{% endif %} \ No newline at end of file diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js index 4dbfbefa13..0c958c65a2 100644 --- a/InvenTree/templates/js/order.js +++ b/InvenTree/templates/js/order.js @@ -139,7 +139,7 @@ function loadPurchaseOrderTable(table, options) { title: '{% trans "Purchase Order" %}', formatter: function(value, row, index, field) { - var prefix = "{% inventree_setting 'PURCHASEORDER_REFERENCE_PREFIX' %}"; + var prefix = "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}"; if (prefix) { value = `${prefix}${value}`; @@ -221,7 +221,7 @@ function loadSalesOrderTable(table, options) { title: '{% trans "Sales Order" %}', formatter: function(value, row, index, field) { - var prefix = "{% inventree_setting 'SALESORDER_REFERENCE_PREFIX' %}"; + var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}"; if (prefix) { value = `${prefix}${value}`;