Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-10-26 07:56:59 +11:00
commit a047b7c6df
37 changed files with 1580 additions and 736 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

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

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:
# Build Order settings
'BUILDORDER_REFERENCE_PREFIX': 'BO',
'BUILDORDER_REFERENCE_REGEX': '',
- 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)
# Purchase Order Settings
'PURCHASEORDER_REFERENCE_PREFIX': 'PO',
The keys must be upper-case
"""
# Sales Order Settings
'SALESORDER_REFERENCE_PREFIX': 'SO',
GLOBAL_SETTINGS = {
'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'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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
@ -16,7 +17,14 @@ register = template.Library()
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()
@ -28,7 +36,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()
@ -41,7 +49,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()
@ -87,8 +95,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>
@ -30,3 +34,4 @@
<a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
</li>
</ul>
{% endif %}

View File

@ -42,7 +42,7 @@ function loadBuildTable(table, options) {
switchable: false,
formatter: function(value, row, index, field) {
var prefix = "{% inventree_setting 'BUILDORDER_REFERENCE_PREFIX' %}";
var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}";
if (prefix) {
value = `${prefix}${value}`;

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