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', ]
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.

View File

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

View File

@ -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:

View File

@ -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

View File

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

View File

@ -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):
"""

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')
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)

View File

@ -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

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.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'),

View File

@ -23,23 +23,27 @@ InvenTree | {% trans "Company" %} - {{ company.name }}
<hr>
<h4>
{{ 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>
{% endif %}
</h4>
<p>{{ company.description }}</p>
<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'>
<span class='fas fa-shopping-cart'/>
</button>
{% endif %}
{% if perms.company.change_company %}
<button type='button' class='btn btn-default' id='company-edit' title='Edit company information'>
<span class='fas fa-edit icon-green'/>
</button>
{% endif %}
{% if perms.company.delete_company %}
<button type='button' class='btn btn-default' id='company-delete' title='Delete company'>
<span class='fas fa-trash-alt icon-red'/>
</button>
{% endif %}
</div>
{% endblock %}

View File

@ -9,17 +9,25 @@
<hr>
{% if roles.purchase_order.change %}
<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>
{% endif %}
<div class="dropdown" style="float: right;">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}
<span class="caret"></span></button>
<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>
{% endif %}
{% if roles.purchase_order.delete %}
<li><a href='#' id='multi-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li>
{% endif %}
</ul>
</div>
</div>
{% endif %}
<table class='table table-striped table-condensed' id='part-table' data-toolbar='#button-toolbar'>
</table>

View File

@ -12,12 +12,13 @@ InvenTree | {% trans "Supplier List" %}
<h3>{{ title }}</h3>
<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 class='btn-group'>
<button type='button' class="btn btn-success" id='new-company'>{{ button_text }}</button>
</div>
</div>
{% endif %}
<table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'>
</table>

View File

@ -9,6 +9,7 @@
<h4>{% trans "Purchase Orders" %}</h4>
<hr>
{% if roles.purchase_order.add %}
<div id='button-bar'>
<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>
@ -17,6 +18,7 @@
</div>
</div>
</div>
{% endif %}
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
</table>

View File

@ -9,6 +9,7 @@
<h4>{% trans "Sales Orders" %}</h4>
<hr>
{% if roles.sales_order.add %}
<div id='button-bar'>
<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>
@ -17,6 +18,7 @@
</div>
</div>
</div>
{% endif %}
<table class='table table-striped table-condensed po-table' id='sales-order-table' data-toolbar='#button-bar'>
</table>

View File

@ -18,19 +18,27 @@ src="{% static 'img/blank_image.png' %}"
{% block page_data %}
<h3>{% trans "Supplier Part" %}</h3>
<p>{{ part.supplier.name }} - {{ part.SKU }}</p>
{% if roles.purchase_order.change %}
<div class='btn-row'>
<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" %}'>
<span class='fas fa-shopping-cart'></span>
</button>
{% endif %}
<button type='button' class='btn btn-default btn-glyph' id='edit-part' title='{% trans "Edit supplier part" %}'>
<span class='fas fa-edit icon-green'/>
</button>
{% if roles.purchase_order.delete %}
<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'/>
</button>
{% endif %}
</div>
</div>
{% endif %}
{% endblock %}
{% block page_details %}

View File

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

View File

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

View File

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

View File

@ -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'

View File

@ -13,9 +13,11 @@
<h4>{% trans "Sales Order Items" %}</h4>
{% if roles.sales_order.change %}
<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>
</div>
{% endif %}
<table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'>

View File

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

View File

@ -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 <part> allocated to <build> """
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()

View File

@ -15,17 +15,8 @@
<table class='table table-striped table-condensed'>
<thead></thead>
<tbody>
<tr>
<th>{% trans "Reference Prefix" %}</th>
<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>
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PREFIX" %}
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_REGEX" %}
</tbody>
</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 %}
<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>
<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 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;
}

View File

@ -10,4 +10,10 @@
{% endblock %}
{% 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 %}

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.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 %}

View File

@ -10,4 +10,12 @@
{% endblock %}
{% 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 %}

View File

@ -9,8 +9,12 @@
<a href="{% url 'settings-theme' %}"><span class='fas fa-fill'></span> {% trans "Theme" %}</a>
</li>
</ul>
{% if user.is_staff %}
<h4><span class='fas fa-cogs'></span> {% trans "InvenTree Settings" %}</h4>
<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 %}>
<a href="{% url 'settings-currency' %}"><span class='fas fa-dollar-sign'></span> {% trans "Currency" %}</a>
</li>
@ -29,4 +33,5 @@
<li {% if tab == 'so' %} class='active'{% endif %}>
<a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
</li>
</ul>
</ul>
{% endif %}

View File

@ -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}`;