Merge remote-tracking branch 'inventree/master' into build-fixes

# Conflicts:
#	InvenTree/locale/de/LC_MESSAGES/django.po
#	InvenTree/locale/en/LC_MESSAGES/django.po
#	InvenTree/locale/es/LC_MESSAGES/django.po
#	InvenTree/templates/js/build.html
This commit is contained in:
Oliver Walters 2020-10-26 22:38:43 +11:00
commit a4f6efc05d
33 changed files with 621 additions and 65 deletions

View File

@ -120,6 +120,19 @@ def str2bool(text, test=True):
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ] 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): def isNull(text):
""" """
Test if a string 'looks' like a null value. Test if a string 'looks' like a null value.

View File

@ -39,6 +39,8 @@ from .views import IndexView, SearchView, DatabaseStatsView
from .views import SettingsView, EditUserView, SetPasswordView, ColorThemeSelectView from .views import SettingsView, EditUserView, SetPasswordView, ColorThemeSelectView
from .views import DynamicJsView from .views import DynamicJsView
from common.views import SettingEdit
from .api import InfoView from .api import InfoView
from .api import ActionPluginView 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'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'), 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'^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'^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'), 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'^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'^sales-order/?', SettingsView.as_view(template_name='InvenTree/settings/so.html'), name='settings-so'),
url(r'^(?P<pk>\d+)/edit/?', SettingEdit.as_view(), name='setting-edit'),
# Catch any other urls # Catch any other urls
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'), url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'),
] ]

View File

@ -128,9 +128,13 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
def has_permission(self): 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 = [] roles_required = []
if type(self.role_required) is str: if type(self.role_required) is str:

View File

@ -1,5 +1,5 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.db.utils import OperationalError, ProgrammingError from django.db.utils import OperationalError, ProgrammingError, IntegrityError
class CommonConfig(AppConfig): class CommonConfig(AppConfig):
@ -32,7 +32,7 @@ class CommonConfig(AppConfig):
return return
# Default instance name # Default instance name
instance_name = 'InvenTree Server' instance_name = InvenTreeSetting.get_default_value('INVENTREE_INSTANCE')
# Use the old name if it exists # Use the old name if it exists
if InvenTreeSetting.objects.filter(key='InstanceName').exists(): if InvenTreeSetting.objects.filter(key='InstanceName').exists():
@ -48,7 +48,7 @@ class CommonConfig(AppConfig):
value=instance_name value=instance_name
) )
except (OperationalError, ProgrammingError): except (OperationalError, ProgrammingError, IntegrityError):
# Migrations have not yet been applied - table does not exist # Migrations have not yet been applied - table does not exist
pass pass
@ -59,12 +59,12 @@ class CommonConfig(AppConfig):
from .models import InvenTreeSetting from .models import InvenTreeSetting
for key in InvenTreeSetting.DEFAULT_VALUES.keys(): for key in InvenTreeSetting.GLOBAL_SETTINGS.keys():
try: try:
settings = InvenTreeSetting.objects.filter(key__iexact=key) settings = InvenTreeSetting.objects.filter(key__iexact=key)
if settings.count() == 0: if settings.count() == 0:
value = InvenTreeSetting.DEFAULT_VALUES[key] value = InvenTreeSetting.get_default_value(key)
print(f"Creating default setting for {key} -> '{value}'") print(f"Creating default setting for {key} -> '{value}'")
@ -87,6 +87,6 @@ class CommonConfig(AppConfig):
setting.key = key setting.key = key
setting.save() setting.save()
except (OperationalError, ProgrammingError): except (OperationalError, ProgrammingError, IntegrityError):
# Table might not yet exist # Table might not yet exist
pass pass

View File

@ -33,6 +33,5 @@ class SettingEditForm(HelperForm):
model = InvenTreeSetting model = InvenTreeSetting
fields = [ fields = [
'key',
'value' 'value'
] ]

View File

@ -11,10 +11,12 @@ import decimal
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import InvenTree.helpers
import InvenTree.fields import InvenTree.fields
@ -27,34 +29,205 @@ class InvenTreeSetting(models.Model):
even if that key does not exist. even if that key does not exist.
""" """
# Dict of default values for various internal settings """
DEFAULT_VALUES = { Dict of all global settings values:
# Global inventree settings
'INVENTREE_INSTANCE': 'InvenTree Server',
# Part settings The key of each item is the name of the value as it appears in the database.
'PART_IPN_REGEX': '',
'PART_COPY_BOM': True,
'PART_COPY_PARAMETERS': True,
'PART_COPY_TESTS': True,
# Stock settings Each global setting has the following parameters:
# Build Order settings - name: Translatable string name of the setting (required)
'BUILDORDER_REFERENCE_PREFIX': 'BO', - description: Translatable string description of the setting (required)
'BUILDORDER_REFERENCE_REGEX': '', - default: Default value (optional)
- units: Units of the particular setting (optional)
- validator: Validation function for the setting (optional)
# Purchase Order Settings The keys must be upper-case
'PURCHASEORDER_REFERENCE_PREFIX': 'PO', """
# Sales Order Settings GLOBAL_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: class Meta:
verbose_name = "InvenTree Setting" verbose_name = "InvenTree Setting"
verbose_name_plural = "InvenTree Settings" 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 @classmethod
def get_setting(cls, key, backup_value=None): def get_setting(cls, key, backup_value=None):
""" """
@ -64,17 +237,14 @@ class InvenTreeSetting(models.Model):
# If no backup value is specified, atttempt to retrieve a "default" value # If no backup value is specified, atttempt to retrieve a "default" value
if backup_value is None: if backup_value is None:
backup_value = InvenTreeSetting.DEFAULT_VALUES.get(key, None) backup_value = cls.get_default_value(key)
try: setting = InvenTreeSetting.get_setting_object(key)
settings = InvenTreeSetting.objects.filter(key__iexact=key)
if len(settings) > 0: if setting:
return settings[0].value return setting.value
else: else:
return backup_value return backup_value
except InvenTreeSetting.DoesNotExist:
return backup_value
@classmethod @classmethod
def set_setting(cls, key, value, user, create=True): def set_setting(cls, key, value, user, create=True):
@ -108,6 +278,59 @@ class InvenTreeSetting(models.Model):
value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) 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): def validate_unique(self, exclude=None):
""" Ensure that the key:value pair is unique. """ Ensure that the key:value pair is unique.
In addition to the base validators, this ensures that the 'key' In addition to the base validators, this ensures that the 'key'
@ -123,6 +346,24 @@ class InvenTreeSetting(models.Model):
except InvenTreeSetting.DoesNotExist: except InvenTreeSetting.DoesNotExist:
pass 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): class Currency(models.Model):
""" """

View File

@ -0,0 +1,14 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
{{ block.super }}
<!--
<p>
<b>{{ name }}</b><br>
{{ description }}<br>
<i>{% trans "Current value" %}: {{ value }}</i>
</p>
-->
{% endblock %}

View File

@ -35,14 +35,37 @@ class SettingsTest(TestCase):
self.client.login(username='username', password='password') 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): def test_defaults(self):
""" """
Populate the settings with default values 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) InvenTreeSetting.set_setting(key, value, self.user)

View File

@ -6,8 +6,10 @@ Django views for interacting with common models
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.forms import CheckboxInput
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.helpers import str2bool
from . import models from . import models
from . import forms from . import forms
@ -46,3 +48,47 @@ class SettingEdit(AjaxUpdateView):
model = models.InvenTreeSetting model = models.InvenTreeSetting
ajax_form_title = _('Change Setting') ajax_form_title = _('Change Setting')
form_class = forms.SettingEditForm 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

View File

@ -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'),
),
]

View File

@ -12,7 +12,7 @@ import math
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models 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.apps import apps
from django.urls import reverse from django.urls import reverse
@ -81,8 +81,11 @@ class Company(models.Model):
class Meta: class Meta:
ordering = ['name', ] 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'), help_text=_('Company name'),
verbose_name=_('Company name')) verbose_name=_('Company name'))
@ -98,7 +101,8 @@ class Company(models.Model):
verbose_name=_('Phone number'), verbose_name=_('Phone number'),
blank=True, help_text=_('Contact 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, contact = models.CharField(max_length=100,
verbose_name=_('Contact'), verbose_name=_('Contact'),

View File

@ -23,23 +23,27 @@ InvenTree | {% trans "Company" %} - {{ company.name }}
<hr> <hr>
<h4> <h4>
{{ company.name }} {{ company.name }}
{% if user.is_staff and roles.company.change %} {% if user.is_staff and perms.company.change_company %}
<a href="{% url 'admin:company_company_change' company.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a> <a href="{% url 'admin:company_company_change' company.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %} {% endif %}
</h4> </h4>
<p>{{ company.description }}</p> <p>{{ company.description }}</p>
<div class='btn-group action-buttons'> <div class='btn-group action-buttons'>
{% if company.is_supplier %} {% if company.is_supplier and roles.purchase_order.add %}
<button type='button' class='btn btn-default' id='company-order-2' title='Create purchase order'> <button type='button' class='btn btn-default' id='company-order-2' title='Create purchase order'>
<span class='fas fa-shopping-cart'/> <span class='fas fa-shopping-cart'/>
</button> </button>
{% endif %} {% endif %}
{% if perms.company.change_company %}
<button type='button' class='btn btn-default' id='company-edit' title='Edit company information'> <button type='button' class='btn btn-default' id='company-edit' title='Edit company information'>
<span class='fas fa-edit icon-green'/> <span class='fas fa-edit icon-green'/>
</button> </button>
{% endif %}
{% if perms.company.delete_company %}
<button type='button' class='btn btn-default' id='company-delete' title='Delete company'> <button type='button' class='btn btn-default' id='company-delete' title='Delete company'>
<span class='fas fa-trash-alt icon-red'/> <span class='fas fa-trash-alt icon-red'/>
</button> </button>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -9,17 +9,25 @@
<hr> <hr>
{% if roles.purchase_order.change %}
<div id='button-toolbar' class='btn-group'> <div id='button-toolbar' class='btn-group'>
{% if roles.purchase_order.add %}
<button class="btn btn-success" id='part-create' title='{% trans "Create new supplier part" %}'>{% trans "New Supplier Part" %}</button> <button class="btn btn-success" id='part-create' title='{% trans "Create new supplier part" %}'>{% trans "New Supplier Part" %}</button>
{% endif %}
<div class="dropdown" style="float: right;"> <div class="dropdown" style="float: right;">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %} <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}
<span class="caret"></span></button> <span class="caret"></span></button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if roles.purchase_order.add %}
<li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li> <li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
{% endif %}
{% if roles.purchase_order.delete %}
<li><a href='#' id='multi-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li> <li><a href='#' id='multi-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
{% endif %}
<table class='table table-striped table-condensed' id='part-table' data-toolbar='#button-toolbar'> <table class='table table-striped table-condensed' id='part-table' data-toolbar='#button-toolbar'>
</table> </table>

View File

@ -12,12 +12,13 @@ InvenTree | {% trans "Supplier List" %}
<h3>{{ title }}</h3> <h3>{{ title }}</h3>
<hr> <hr>
{% if pagetype == 'manufacturers' and roles.purchase_order.add or pagetype == 'suppliers' and roles.purchase_order.add or pagetype == 'customers' and roles.sales_order.add %}
<div id='button-toolbar'> <div id='button-toolbar'>
<div class='btn-group'> <div class='btn-group'>
<button type='button' class="btn btn-success" id='new-company'>{{ button_text }}</button> <button type='button' class="btn btn-success" id='new-company'>{{ button_text }}</button>
</div> </div>
</div> </div>
{% endif %}
<table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'> <table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'>
</table> </table>

View File

@ -9,6 +9,7 @@
<h4>{% trans "Purchase Orders" %}</h4> <h4>{% trans "Purchase Orders" %}</h4>
<hr> <hr>
{% if roles.purchase_order.add %}
<div id='button-bar'> <div id='button-bar'>
<div class='button-toolbar container-fluid' style='float: right;'> <div class='button-toolbar container-fluid' style='float: right;'>
<button class='btn btn-primary' type='button' id='company-order2' title='{% trans "Create new purchase order" %}'>{% trans "New Purchase Order" %}</button> <button class='btn btn-primary' type='button' id='company-order2' title='{% trans "Create new purchase order" %}'>{% trans "New Purchase Order" %}</button>
@ -17,6 +18,7 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'> <table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
</table> </table>

View File

@ -9,6 +9,7 @@
<h4>{% trans "Sales Orders" %}</h4> <h4>{% trans "Sales Orders" %}</h4>
<hr> <hr>
{% if roles.sales_order.add %}
<div id='button-bar'> <div id='button-bar'>
<div class='button-toolbar container-fluid' style='float: right;'> <div class='button-toolbar container-fluid' style='float: right;'>
<button class='btn btn-primary' type='button' id='new-sales-order' title='{% trans "Create new sales order" %}'>{% trans "New Sales Order" %}</button> <button class='btn btn-primary' type='button' id='new-sales-order' title='{% trans "Create new sales order" %}'>{% trans "New Sales Order" %}</button>
@ -17,6 +18,7 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
<table class='table table-striped table-condensed po-table' id='sales-order-table' data-toolbar='#button-bar'> <table class='table table-striped table-condensed po-table' id='sales-order-table' data-toolbar='#button-bar'>
</table> </table>

View File

@ -18,19 +18,27 @@ src="{% static 'img/blank_image.png' %}"
{% block page_data %} {% block page_data %}
<h3>{% trans "Supplier Part" %}</h3> <h3>{% trans "Supplier Part" %}</h3>
<p>{{ part.supplier.name }} - {{ part.SKU }}</p> <p>{{ part.supplier.name }} - {{ part.SKU }}</p>
{% if roles.purchase_order.change %}
<div class='btn-row'> <div class='btn-row'>
<div class='btn-group action-buttons' role='group'> <div class='btn-group action-buttons' role='group'>
{% if roles.purchase_order.add %}
<button type='button' class='btn btn-default btn-glyph' id='order-part' title='{% trans "Order part" %}'> <button type='button' class='btn btn-default btn-glyph' id='order-part' title='{% trans "Order part" %}'>
<span class='fas fa-shopping-cart'></span> <span class='fas fa-shopping-cart'></span>
</button> </button>
{% endif %}
<button type='button' class='btn btn-default btn-glyph' id='edit-part' title='{% trans "Edit supplier part" %}'> <button type='button' class='btn btn-default btn-glyph' id='edit-part' title='{% trans "Edit supplier part" %}'>
<span class='fas fa-edit icon-green'/> <span class='fas fa-edit icon-green'/>
</button> </button>
{% if roles.purchase_order.delete %}
<button type='button' class='btn btn-default btn-glyph' id='delete-part' title='{% trans "Delete supplier part" %}'> <button type='button' class='btn btn-default btn-glyph' id='delete-part' title='{% trans "Delete supplier part" %}'>
<span class='fas fa-trash-alt icon-red'/> <span class='fas fa-trash-alt icon-red'/>
</button> </button>
{% endif %}
</div> </div>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}
{% block page_details %} {% block page_details %}

View File

@ -10,11 +10,13 @@
<hr> <hr>
{% if roles.purchase_order.add %}
<div id='button-bar'> <div id='button-bar'>
<div class='btn-group'> <div class='btn-group'>
<button class='btn btn-primary' type='button' id='order-part2' title='Order part'>Order Part</button> <button class='btn btn-primary' type='button' id='order-part2' title='Order part'>Order Part</button>
</div> </div>
</div> </div>
{% endif %}
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'> <table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
</table> </table>

View File

@ -11,9 +11,11 @@
<hr> <hr>
{% if roles.purchase_order.add %}
<div id='price-break-toolbar' class='btn-group'> <div id='price-break-toolbar' class='btn-group'>
<button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button> <button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button>
</div> </div>
{% endif %}
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'> <table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>
</table> </table>

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from .models import SupplierPart from .models import SupplierPart
@ -25,7 +26,24 @@ class CompanyViewTest(TestCase):
# Create a user # Create a user
User = get_user_model() 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') self.client.login(username='username', password='password')

View File

@ -14,6 +14,7 @@ from django.forms import HiddenInput
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
from InvenTree.views import InvenTreeRoleMixin
from common.models import Currency from common.models import Currency
@ -29,7 +30,7 @@ from .forms import EditSupplierPartForm
from .forms import EditPriceBreakForm from .forms import EditPriceBreakForm
class CompanyIndex(ListView): class CompanyIndex(InvenTreeRoleMixin, ListView):
""" View for displaying list of companies """ View for displaying list of companies
""" """
@ -37,6 +38,7 @@ class CompanyIndex(ListView):
template_name = 'company/index.html' template_name = 'company/index.html'
context_object_name = 'companies' context_object_name = 'companies'
paginate_by = 50 paginate_by = 50
permission_required = 'company.view_company'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -116,8 +118,8 @@ class CompanyNotes(UpdateView):
context_object_name = 'company' context_object_name = 'company'
template_name = 'company/notes.html' template_name = 'company/notes.html'
model = Company model = Company
fields = ['notes'] fields = ['notes']
permission_required = 'company.view_company'
def get_success_url(self): def get_success_url(self):
return reverse('company-notes', kwargs={'pk': self.get_object().id}) return reverse('company-notes', kwargs={'pk': self.get_object().id})
@ -137,6 +139,7 @@ class CompanyDetail(DetailView):
template_name = 'company/detail.html' template_name = 'company/detail.html'
queryset = Company.objects.all() queryset = Company.objects.all()
model = Company model = Company
permission_required = 'company.view_company'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
@ -150,6 +153,7 @@ class CompanyImage(AjaxUpdateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Update Company Image') ajax_form_title = _('Update Company Image')
form_class = CompanyImageForm form_class = CompanyImageForm
permission_required = 'company.change_company'
def get_data(self): def get_data(self):
return { return {
@ -164,6 +168,7 @@ class CompanyEdit(AjaxUpdateView):
context_object_name = 'company' context_object_name = 'company'
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Company') ajax_form_title = _('Edit Company')
permission_required = 'company.change_company'
def get_data(self): def get_data(self):
return { return {
@ -177,6 +182,7 @@ class CompanyCreate(AjaxCreateView):
context_object_name = 'company' context_object_name = 'company'
form_class = EditCompanyForm form_class = EditCompanyForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
permission_required = 'company.add_company'
def get_form_title(self): def get_form_title(self):
@ -230,6 +236,7 @@ class CompanyDelete(AjaxDeleteView):
ajax_template_name = 'company/delete.html' ajax_template_name = 'company/delete.html'
ajax_form_title = _('Delete Company') ajax_form_title = _('Delete Company')
context_object_name = 'company' context_object_name = 'company'
permission_required = 'company.delete_company'
def get_data(self): def get_data(self):
return { return {
@ -243,6 +250,7 @@ class SupplierPartDetail(DetailView):
template_name = 'company/supplier_part_detail.html' template_name = 'company/supplier_part_detail.html'
context_object_name = 'part' context_object_name = 'part'
queryset = SupplierPart.objects.all() queryset = SupplierPart.objects.all()
permission_required = 'purchase_order.view'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
@ -258,6 +266,7 @@ class SupplierPartEdit(AjaxUpdateView):
form_class = EditSupplierPartForm form_class = EditSupplierPartForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Supplier Part') ajax_form_title = _('Edit Supplier Part')
role_required = 'purchase_order.change'
class SupplierPartCreate(AjaxCreateView): class SupplierPartCreate(AjaxCreateView):
@ -268,6 +277,7 @@ class SupplierPartCreate(AjaxCreateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Create new Supplier Part') ajax_form_title = _('Create new Supplier Part')
context_object_name = 'part' context_object_name = 'part'
role_required = 'purchase_order.add'
def get_form(self): def get_form(self):
""" Create Form instance to create a new SupplierPart object. """ Create Form instance to create a new SupplierPart object.
@ -327,6 +337,7 @@ class SupplierPartDelete(AjaxDeleteView):
success_url = '/supplier/' success_url = '/supplier/'
ajax_template_name = 'company/partdelete.html' ajax_template_name = 'company/partdelete.html'
ajax_form_title = _('Delete Supplier Part') ajax_form_title = _('Delete Supplier Part')
role_required = 'purchase_order.delete'
parts = [] parts = []
@ -398,6 +409,7 @@ class PriceBreakCreate(AjaxCreateView):
form_class = EditPriceBreakForm form_class = EditPriceBreakForm
ajax_form_title = _('Add Price Break') ajax_form_title = _('Add Price Break')
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
role_required = 'purchase_order.add'
def get_data(self): def get_data(self):
return { return {
@ -440,6 +452,7 @@ class PriceBreakEdit(AjaxUpdateView):
form_class = EditPriceBreakForm form_class = EditPriceBreakForm
ajax_form_title = _('Edit Price Break') ajax_form_title = _('Edit Price Break')
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
role_required = 'purchase_order.change'
def get_form(self): def get_form(self):
@ -455,3 +468,4 @@ class PriceBreakDelete(AjaxDeleteView):
model = SupplierPriceBreak model = SupplierPriceBreak
ajax_form_title = _("Delete Price Break") ajax_form_title = _("Delete Price Break")
ajax_template_name = 'modal_delete_form.html' ajax_template_name = 'modal_delete_form.html'
role_required = 'purchase_order.delete'

View File

@ -13,9 +13,11 @@
<h4>{% trans "Sales Order Items" %}</h4> <h4>{% trans "Sales Order Items" %}</h4>
{% if roles.sales_order.change %}
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'> <div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
<button type='button' class='btn btn-default' id='new-so-line'>{% trans "Add Line Item" %}</button> <button type='button' class='btn btn-default' id='new-so-line'>{% trans "Add Line Item" %}</button>
</div> </div>
{% endif %}
<table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'> <table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'>

View File

@ -37,7 +37,9 @@
<hr> <hr>
<div class='panel panel-default'> <div class='panel panel-default'>
<div class='panel-content'> <div class='panel-content'>
{% if part.notes %}
{{ part.notes | markdownify }} {{ part.notes | markdownify }}
{% endif %}
</div> </div>
</div> </div>

View File

@ -5,7 +5,8 @@ import os
from django import template from django import template
from InvenTree import version, settings from InvenTree import version, settings
from InvenTree.helpers import decimal2string
import InvenTree.helpers
from common.models import InvenTreeSetting, ColorTheme from common.models import InvenTreeSetting, ColorTheme
@ -29,7 +30,14 @@ def define(value, *args, **kwargs):
def decimal(x, *args, **kwargs): def decimal(x, *args, **kwargs):
""" Simplified rendering of a decimal number """ """ 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() @register.simple_tag()
@ -41,7 +49,7 @@ def inrange(n, *args, **kwargs):
@register.simple_tag() @register.simple_tag()
def multiply(x, y, *args, **kwargs): def multiply(x, y, *args, **kwargs):
""" Multiply two numbers together """ """ Multiply two numbers together """
return decimal2string(x * y) return InvenTree.helpers.decimal2string(x * y)
@register.simple_tag() @register.simple_tag()
@ -54,7 +62,7 @@ def add(x, y, *args, **kwargs):
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> """
return decimal2string(build.getAllocatedQuantity(part)) return InvenTree.helpers.decimal2string(build.getAllocatedQuantity(part))
@register.simple_tag() @register.simple_tag()
@ -100,8 +108,15 @@ def inventree_docs_url(*args, **kwargs):
@register.simple_tag() @register.simple_tag()
def inventree_setting(key, *args, **kwargs): def setting_object(key, *args, **kwargs):
return InvenTreeSetting.get_setting(key, backup_value=kwargs.get('backup', None)) """
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() @register.simple_tag()

View File

@ -15,17 +15,8 @@
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<thead></thead> <thead></thead>
<tbody> <tbody>
<tr> {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PREFIX" %}
<th>{% trans "Reference Prefix" %}</th> {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_REGEX" %}
<th>{% inventree_setting 'BUILDORDER_REFERENCE_PREFIX' backup='BO' %}</th>
<td>{% trans "Prefix for Build Order reference" %}</td>
</tr>
<tr>
<th>{% trans "Reference Regex" %}</th>
<th>{% inventree_setting 'BUILDORDER_REFERENCE_REGEX' %}</th>
<td>{% trans "Regex validator for Build Order reference" %}</td>
<td></td>
</tr>
</tbody> </tbody>
</table> </table>

View File

@ -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 %}
<table class='table table-striped table-condensed'>
<thead></thead>
<tbody>
{% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" %}
{% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" %}
</tbody>
</table>
{% endblock %}

View File

@ -11,6 +11,16 @@
{% block settings %} {% block settings %}
<table class='table table-striped table-condensed'>
<thead></thead>
<tbody>
{% 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" %}
</tbody>
</table>
<h4>{% trans "Part Parameter Templates" %}</h4> <h4>{% trans "Part Parameter Templates" %}</h4>
<div id='param-buttons'> <div id='param-buttons'>
@ -53,7 +63,7 @@
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>"; var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>"; var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
var html = "<div class='btn-group' role='group'>" + bEdit + bDel + "</div>"; var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
return html; return html;
} }

View File

@ -10,4 +10,10 @@
{% endblock %} {% endblock %}
{% block settings %} {% block settings %}
<table class='table table-striped table-condensed'>
<thead></thead>
<tbody>
{% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_REFERENCE_PREFIX" %}
</tbody>
</table>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,29 @@
{% load inventree_extras %}
{% load i18n %}
{% setting_object key as setting %}
<tr>
<td><b>{{ setting.name }}</b></td>
<td>
{% if setting.is_bool %}
<div>
<input fieldname='{{ setting.key }}' class='slidey' type='checkbox' data-offstyle='warning' data-onstyle='success' data-size='small' data-toggle='toggle' disabled autocomplete='off' {% if setting.as_bool %}checked=''{% endif %}>
</div>
{% else %}
{% if setting.value %}
<b>{{ setting.value }}</b>{{ setting.units }}</td>
{% else %}
<i>{% trans "No value set" %}</i>
{% endif %}
{% endif %}
<td>
{{ setting.description }}
</td>
<td>
<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" %}'>
<span class='fas fa-edit icon-green'></span>
</button>
</div>
</td>
</tr>

View File

@ -37,3 +37,20 @@ InvenTree | {% trans "Settings" %}
{% block js_load %} {% block js_load %}
{{ block.super }} {{ block.super }}
{% endblock %} {% 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 %}

View File

@ -10,4 +10,12 @@
{% endblock %} {% endblock %}
{% block settings %} {% block settings %}
<table class='table table-striped table-condensed'>
<thead></thead>
<tbody>
{% include "InvenTree/settings/setting.html" with key="SALESORDER_REFERENCE_PREFIX" %}
</tbody>
</table>
{% endblock %} {% endblock %}

View File

@ -9,8 +9,12 @@
<a href="{% url 'settings-theme' %}"><span class='fas fa-fill'></span> {% trans "Theme" %}</a> <a href="{% url 'settings-theme' %}"><span class='fas fa-fill'></span> {% trans "Theme" %}</a>
</li> </li>
</ul> </ul>
{% 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>
<ul class='nav nav-pills nav-stacked'> <ul class='nav nav-pills nav-stacked'>
<li {% if tab == 'global' %} class='active' {% endif %}>
<a href='{% url "settings-global" %}'><span class='fas fa-globe'></span> {% trans "Global" %}</a>
</li>
<li{% ifequal tab 'currency' %} class='active'{% endifequal %}> <li{% ifequal tab 'currency' %} class='active'{% endifequal %}>
<a href="{% url 'settings-currency' %}"><span class='fas fa-dollar-sign'></span> {% trans "Currency" %}</a> <a href="{% url 'settings-currency' %}"><span class='fas fa-dollar-sign'></span> {% trans "Currency" %}</a>
</li> </li>
@ -30,3 +34,4 @@
<a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a> <a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
</li> </li>
</ul> </ul>
{% endif %}

View File

@ -139,7 +139,7 @@ function loadPurchaseOrderTable(table, options) {
title: '{% trans "Purchase Order" %}', title: '{% trans "Purchase Order" %}',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var prefix = "{% inventree_setting 'PURCHASEORDER_REFERENCE_PREFIX' %}"; var prefix = "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}";
if (prefix) { if (prefix) {
value = `${prefix}${value}`; value = `${prefix}${value}`;
@ -221,7 +221,7 @@ function loadSalesOrderTable(table, options) {
title: '{% trans "Sales Order" %}', title: '{% trans "Sales Order" %}',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var prefix = "{% inventree_setting 'SALESORDER_REFERENCE_PREFIX' %}"; var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
if (prefix) { if (prefix) {
value = `${prefix}${value}`; value = `${prefix}${value}`;