Merge branch 'master' of github.com:inventree/InvenTree into exchange_rate_task

This commit is contained in:
eeintech 2021-08-04 10:22:14 -04:00
commit 1b79ef940e
110 changed files with 102445 additions and 29536 deletions

2
.gitattributes vendored
View File

@ -7,5 +7,5 @@
*.yml text
*.yaml text
*.conf text
*.sh text
*.sh text eol=lf
*.js text

28
.github/workflows/javascript.yaml vendored Normal file
View File

@ -0,0 +1,28 @@
# Check javascript template files
name: Javascript Templates
on:
push:
branches:
- master
pull_request:
branches-ignore:
- l10*
jobs:
javascript:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Check Files
run: |
cd ci
python check_js_templates.py

View File

@ -1,4 +1,5 @@
from common.settings import currency_code_default, currency_codes
from urllib.error import HTTPError, URLError
from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
@ -26,4 +27,8 @@ class InvenTreeExchange(SimpleExchangeBackend):
symbols = ','.join(currency_codes())
super().update_rates(base=base_currency, symbols=symbols)
try:
super().update_rates(base=base_currency, symbols=symbols)
# catch connection errors
except (HTTPError, URLError):
print('Encountered connection error while updating')

View File

@ -56,7 +56,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
def __init__(self, **kwargs):
# detect if creating migration
if 'makemigrations' in sys.argv:
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
# remove currency information for a clean migration
kwargs['default_currency'] = ''
kwargs['currency_choices'] = []

View File

@ -13,7 +13,6 @@ from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
from common.models import ColorTheme
from part.models import PartCategory
@ -177,39 +176,6 @@ class SetPasswordForm(HelperForm):
]
class ColorThemeSelectForm(forms.ModelForm):
""" Form for setting color theme """
name = forms.ChoiceField(choices=(), required=False)
class Meta:
model = ColorTheme
fields = [
'name'
]
def __init__(self, *args, **kwargs):
super(ColorThemeSelectForm, self).__init__(*args, **kwargs)
# Populate color themes choices
self.fields['name'].choices = ColorTheme.get_color_themes_choices()
self.helper = FormHelper()
# Form rendering
self.helper.form_show_labels = False
self.helper.layout = Layout(
Div(
Div(Field('name'),
css_class='col-sm-6',
style='width: 200px;'),
Div(StrictButton(_('Apply Theme'), css_class='btn btn-primary', type='submit'),
css_class='col-sm-6',
style='width: auto;'),
css_class='row',
),
)
class SettingCategorySelectForm(forms.ModelForm):
""" Form for setting category settings """

View File

@ -0,0 +1,38 @@
"""
Custom management command to cleanup old settings that are not defined anymore
"""
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""
Cleanup old (undefined) settings in the database
"""
def handle(self, *args, **kwargs):
print("Collecting settings")
from common.models import InvenTreeSetting, InvenTreeUserSetting
# general settings
db_settings = InvenTreeSetting.objects.all()
model_settings = InvenTreeSetting.GLOBAL_SETTINGS
# check if key exist and delete if not
for setting in db_settings:
if setting.key not in model_settings:
setting.delete()
print(f"deleted setting '{setting.key}'")
# user settings
db_settings = InvenTreeUserSetting.objects.all()
model_settings = InvenTreeUserSetting.GLOBAL_SETTINGS
# check if key exist and delete if not
for setting in db_settings:
if setting.key not in model_settings:
setting.delete()
print(f"deleted user setting '{setting.key}'")
print("checked all settings")

View File

@ -12,6 +12,7 @@ database setup in this file.
"""
import logging
import os
import random
import string
@ -202,7 +203,7 @@ STATICFILES_DIRS = [
# Translated Template settings
STATICFILES_I18_PREFIX = 'i18n'
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js')
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js', 'translated')
STATICFILES_I18_TRG = STATICFILES_DIRS[0] + '_' + STATICFILES_I18_PREFIX
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
STATICFILES_I18_TRG = os.path.join(STATICFILES_I18_TRG, STATICFILES_I18_PREFIX)
@ -347,10 +348,22 @@ REST_FRAMEWORK = {
WSGI_APPLICATION = 'InvenTree.wsgi.application'
background_workers = os.environ.get('INVENTREE_BACKGROUND_WORKERS', None)
if background_workers is not None:
try:
background_workers = int(background_workers)
except ValueError:
background_workers = None
if background_workers is None:
# Sensible default?
background_workers = 4
# django-q configuration
Q_CLUSTER = {
'name': 'InvenTree',
'workers': 4,
'workers': background_workers,
'timeout': 90,
'retry': 120,
'queue_limit': 50,
@ -502,11 +515,24 @@ LANGUAGE_CODE = CONFIG.get('language', 'en-us')
# If a new language translation is supported, it must be added here
LANGUAGES = [
('en', _('English')),
('fr', _('French')),
('de', _('German')),
('el', _('Greek')),
('en', _('English')),
('es', _('Spanish')),
('fr', _('French')),
('he', _('Hebrew')),
('it', _('Italian')),
('ja', _('Japanese')),
('ko', _('Korean')),
('nl', _('Dutch')),
('no', _('Norwegian')),
('pl', _('Polish')),
('ru', _('Russian')),
('sv', _('Swedish')),
('th', _('Thai')),
('tr', _('Turkish')),
('vi', _('Vietnamese')),
('zh-cn', _('Chinese')),
]
# Currencies available for use

View File

@ -39,11 +39,11 @@ from rest_framework.documentation import include_docs_urls
from .views import auth_request
from .views import IndexView, SearchView, DatabaseStatsView
from .views import SettingsView, EditUserView, SetPasswordView
from .views import CurrencySettingsView, CurrencyRefreshView
from .views import CurrencyRefreshView
from .views import AppearanceSelectView, SettingCategorySelectView
from .views import DynamicJsView
from common.views import SettingEdit
from common.views import SettingEdit, UserSettingEdit
from .api import InfoView, NotFoundView
from .api import ActionPluginView
@ -79,49 +79,47 @@ apipatterns = [
settings_urls = [
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
url(r'^i18n/?', include('django.conf.urls.i18n')),
url(r'^global/', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
url(r'^report/', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),
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'^build/', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'),
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'^currencies/', CurrencySettingsView.as_view(), name='settings-currencies'),
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),
url(r'^(?P<pk>\d+)/edit/user', UserSettingEdit.as_view(), name='user-setting-edit'),
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
# 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/settings.html'), name='settings'),
]
# Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer
# These javascript files are served "dynamically" - i.e. rendered on demand
dynamic_javascript_urls = [
url(r'^api.js', DynamicJsView.as_view(template_name='js/api.js'), name='api.js'),
url(r'^attachment.js', DynamicJsView.as_view(template_name='js/attachment.js'), name='attachment.js'),
url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'),
url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'),
url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'),
url(r'^calendar.js', DynamicJsView.as_view(template_name='js/calendar.js'), name='calendar.js'),
url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'),
url(r'^filters.js', DynamicJsView.as_view(template_name='js/filters.js'), name='filters.js'),
url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'),
url(r'^inventree.js', DynamicJsView.as_view(template_name='js/inventree.js'), name='inventree.js'),
url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'),
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/model_renderers.js'), name='model_renderers.js'),
url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'),
url(r'^nav.js', DynamicJsView.as_view(template_name='js/nav.js'), name='nav.js'),
url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'),
url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'),
url(r'^report.js', DynamicJsView.as_view(template_name='js/report.js'), name='report.js'),
url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
url(r'^tables.js', DynamicJsView.as_view(template_name='js/tables.js'), name='tables.js'),
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
url(r'^inventree.js', DynamicJsView.as_view(template_name='js/dynamic/inventree.js'), name='inventree.js'),
url(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
url(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'),
url(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
]
# These javascript files are pased through the Django translation layer
translated_javascript_urls = [
url(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
url(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
url(r'^barcode.js', DynamicJsView.as_view(template_name='js/translated/barcode.js'), name='barcode.js'),
url(r'^bom.js', DynamicJsView.as_view(template_name='js/translated/bom.js'), name='bom.js'),
url(r'^build.js', DynamicJsView.as_view(template_name='js/translated/build.js'), name='build.js'),
url(r'^company.js', DynamicJsView.as_view(template_name='js/translated/company.js'), name='company.js'),
url(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'),
url(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'),
url(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'),
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'),
url(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'),
url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
]
urlpatterns = [
@ -130,7 +128,8 @@ urlpatterns = [
url(r'^supplier-part/', include(supplier_part_urls)),
# "Dynamic" javascript files which are rendered using InvenTree templating.
url(r'^dynamic/', include(dynamic_javascript_urls)),
url(r'^js/dynamic/', include(dynamic_javascript_urls)),
url(r'^js/i18n/', include(translated_javascript_urls)),
url(r'^common/', include(common_urls)),

View File

@ -8,7 +8,7 @@ import re
import common.models
INVENTREE_SW_VERSION = "0.3.1"
INVENTREE_SW_VERSION = "0.5.0 pre"
INVENTREE_API_VERSION = 8

View File

@ -12,6 +12,7 @@ from django.utils.translation import gettext_lazy as _
from django.template.loader import render_to_string
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.urls import reverse_lazy
from django.shortcuts import redirect
from django.contrib.auth.mixins import PermissionRequiredMixin
@ -28,7 +29,7 @@ from common.models import InvenTreeSetting, ColorTheme
from users.models import check_user_role, RuleSet
from .forms import DeleteForm, EditUserForm, SetPasswordForm
from .forms import ColorThemeSelectForm, SettingCategorySelectForm
from .forms import SettingCategorySelectForm
from .helpers import str2bool
from rest_framework import views
@ -777,7 +778,7 @@ class SettingsView(TemplateView):
""" View for configuring User settings
"""
template_name = "InvenTree/settings.html"
template_name = "InvenTree/settings/settings.html"
def get_context_data(self, **kwargs):
@ -785,6 +786,20 @@ class SettingsView(TemplateView):
ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')
ctx["base_currency"] = currency_code_default()
ctx["currencies"] = currency_codes
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")
ctx["categories"] = PartCategory.objects.all().order_by('tree_id', 'lft', 'name')
# When were the rates last updated?
try:
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
ctx["rates_updated"] = backend.last_update
except:
ctx["rates_updated"] = None
return ctx
@ -808,43 +823,12 @@ class CurrencyRefreshView(RedirectView):
# Run it
run_task(taskname)
return self.get(request, *args, **kwargs)
return redirect(reverse_lazy('settings'))
class CurrencySettingsView(TemplateView):
"""
View for configuring currency settings
"""
template_name = "InvenTree/settings/currencies.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs).copy()
ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')
ctx["base_currency"] = currency_code_default()
ctx["currencies"] = currency_codes
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")
# When were the rates last updated?
try:
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
ctx["rates_updated"] = backend.last_update
except:
ctx["rates_updated"] = None
return ctx
class AppearanceSelectView(FormView):
class AppearanceSelectView(RedirectView):
""" View for selecting a color theme """
form_class = ColorThemeSelectForm
success_url = reverse_lazy('settings-appearance')
template_name = "InvenTree/settings/appearance.html"
def get_user_theme(self):
""" Get current user color theme """
try:
@ -854,40 +838,10 @@ class AppearanceSelectView(FormView):
return user_theme
def get_initial(self):
""" Select current user color theme as initial choice """
initial = super(AppearanceSelectView, self).get_initial()
user_theme = self.get_user_theme()
if user_theme:
initial['name'] = user_theme.name
return initial
def get(self, request, *args, **kwargs):
""" Check if current color theme exists, else display alert box """
context = {}
form = self.get_form()
context['form'] = form
user_theme = self.get_user_theme()
if user_theme:
# Check color theme is a valid choice
if not ColorTheme.is_valid_choice(user_theme):
user_color_theme_name = user_theme.name
if not user_color_theme_name:
user_color_theme_name = 'default'
context['invalid_color_theme'] = user_color_theme_name
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
""" Save user color theme selection """
form = self.get_form()
theme = request.POST.get('theme', None)
# Get current user theme
user_theme = self.get_user_theme()
@ -897,20 +851,10 @@ class AppearanceSelectView(FormView):
user_theme = ColorTheme()
user_theme.user = request.user
if form.is_valid():
theme_selected = form.cleaned_data['name']
user_theme.name = theme
user_theme.save()
# Set color theme to form selection
user_theme.name = theme_selected
user_theme.save()
return self.form_valid(form)
else:
# Set color theme to default
user_theme.name = ColorTheme.default_color_theme[0]
user_theme.save()
return self.form_invalid(form)
return redirect(reverse_lazy('settings'))
class SettingCategorySelectView(FormView):

View File

@ -111,8 +111,8 @@ src="{% static 'img/blank_image.png' %}"
<li><a href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
{% endif %}
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
<li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build"% }</a>
{% endif %}
<li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build" %}</a>
{% endif %}
</ul>
</div>
{% endif %}

View File

@ -82,7 +82,7 @@
},
{
success: function(response) {
var prefix = '{% settings_value "BUILDORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {

View File

@ -5,7 +5,7 @@ from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import InvenTreeSetting
from .models import InvenTreeSetting, InvenTreeUserSetting
class SettingsAdmin(ImportExportModelAdmin):
@ -13,4 +13,10 @@ class SettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value')
class UserSettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value', 'user', )
admin.site.register(InvenTreeSetting, SettingsAdmin)
admin.site.register(InvenTreeUserSetting, UserSettingsAdmin)

View File

@ -0,0 +1,33 @@
# Generated by Django 3.2.4 on 2021-07-22 21:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('common', '0010_migrate_currency_setting'),
]
operations = [
migrations.CreateModel(
name='InvenTreeUserSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.CharField(blank=True, help_text='Settings value', max_length=200)),
('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50)),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'InvenTree User Setting',
'verbose_name_plural': 'InvenTree User Settings',
},
),
migrations.AddConstraint(
model_name='inventreeusersetting',
constraint=models.UniqueConstraint(fields=('key', 'user'), name='unique key and user'),
),
]

File diff suppressed because it is too large Load Diff

View File

@ -45,11 +45,28 @@ class SettingEdit(AjaxUpdateView):
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)
ctx['name'] = self.model.get_setting_name(setting.key)
ctx['description'] = self.model.get_setting_description(setting.key)
return ctx
def get_data(self):
"""
Custom data to return to the client after POST success
"""
data = {}
setting = self.get_object()
data['pk'] = setting.pk
data['key'] = setting.key
data['value'] = setting.value
data['is_bool'] = setting.is_bool()
data['is_int'] = setting.is_int()
return data
def get_form(self):
"""
Override default get_form behaviour
@ -69,12 +86,12 @@ class SettingEdit(AjaxUpdateView):
self.object.value = str2bool(setting.value)
form.fields['value'].value = str2bool(setting.value)
name = models.InvenTreeSetting.get_setting_name(setting.key)
name = self.model.get_setting_name(setting.key)
if name:
form.fields['value'].label = name
description = models.InvenTreeSetting.get_setting_description(setting.key)
description = self.model.get_setting_description(setting.key)
if description:
form.fields['value'].help_text = description
@ -111,6 +128,18 @@ class SettingEdit(AjaxUpdateView):
form.add_error('value', _('Supplied value must be a boolean'))
class UserSettingEdit(SettingEdit):
"""
View for editing an InvenTree key:value user settings object,
(or creating it if the key does not already exist)
"""
model = models.InvenTreeUserSetting
ajax_form_title = _('Change User Setting')
form_class = forms.SettingEditForm
ajax_template_name = "common/edit_setting.html"
class MultiStepFormView(SessionWizardView):
""" Setup basic methods of multi-step form

View File

@ -6,6 +6,8 @@ Provides a JSON API for the Company app
from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
from rest_framework import filters
from rest_framework import generics
@ -84,6 +86,23 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset
class ManufacturerPartFilter(rest_filters.FilterSet):
"""
Custom API filters for the ManufacturerPart list endpoint.
"""
class Meta:
model = ManufacturerPart
fields = [
'manufacturer',
'MPN',
'part',
]
# Filter by 'active' status of linked part
active = rest_filters.BooleanFilter(field_name='part__active')
class ManufacturerPartList(generics.ListCreateAPIView):
""" API endpoint for list view of ManufacturerPart object
@ -98,6 +117,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
)
serializer_class = ManufacturerPartSerializer
filterset_class = ManufacturerPartFilter
def get_serializer(self, *args, **kwargs):
@ -115,45 +135,12 @@ class ManufacturerPartList(generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs)
def filter_queryset(self, queryset):
"""
Custom filtering for the queryset.
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Filter by manufacturer
manufacturer = params.get('manufacturer', None)
if manufacturer is not None:
queryset = queryset.filter(manufacturer=manufacturer)
# Filter by parent part?
part = params.get('part', None)
if part is not None:
queryset = queryset.filter(part=part)
# Filter by 'active' status of the part?
active = params.get('active', None)
if active is not None:
active = str2bool(active)
queryset = queryset.filter(part__active=active)
return queryset
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filter_fields = [
]
search_fields = [
'manufacturer__name',
'description',

View File

@ -198,17 +198,16 @@
);
});
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
if (global_settings.INVENTREE_DOWNLOAD_FROM_URL) {
{% if allow_download %}
$('#company-image-url').click(function() {
launchModalForm(
'{% url "company-image-download" company.id %}',
{
reload: true,
}
)
});
{% endif %}
$('#company-image-url').click(function() {
launchModalForm(
'{% url "company-image-download" company.id %}',
{
reload: true,
}
)
});
}
{% endblock %}

View File

@ -371,7 +371,7 @@
requests.push(inventreeDelete(url));
});
$.when.apply($, requests).then(function() {
$.when.apply($, requests).done(function() {
$('#supplier-part-table').bootstrapTable('refresh');
});
}

View File

@ -207,7 +207,7 @@ $("#supplier-part-delete").click(function() {
requests.push(inventreeDelete(url));
});
$.when.apply($, requests).then(function() {
$.when.apply($, requests).done(function() {
reloadSupplierPartTable();
});
}
@ -247,7 +247,7 @@ $("#multi-parameter-delete").click(function() {
requests.push(inventreeDelete(url));
});
$.when.apply($, requests).then(function() {
$.when.apply($, requests).done(function() {
$('#parameter-table').bootstrapTable('refresh');
});
}

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

@ -164,7 +164,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-po-detail" order.pk %}', {
fields: {
reference: {
prefix: "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
},
{% if order.lines.count == 0 and order.status == PurchaseOrderStatus.PENDING %}
supplier: {

View File

@ -66,7 +66,7 @@
},
{
success: function(response) {
var prefix = '{% settings_value "PURCHASEORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {

View File

@ -157,7 +157,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-so-detail" order.pk %}', {
fields: {
reference: {
prefix: "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
},
{% if order.lines.count == 0 and order.status == SalesOrderStatus.PENDING %}
customer: {

View File

@ -67,7 +67,7 @@
{
success: function(response) {
var prefix = '{% settings_value "SALESORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {
var order = response[idx];

View File

@ -159,7 +159,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = PartCategory.objects.all()
class CategoryParameters(generics.ListAPIView):
class CategoryParameterList(generics.ListAPIView):
""" API endpoint for accessing a list of PartCategoryParameterTemplate objects.
- GET: Return a list of PartCategoryParameterTemplate objects
@ -176,30 +176,27 @@ class CategoryParameters(generics.ListAPIView):
- Allow traversing all parent categories
"""
try:
cat_id = int(self.kwargs.get('pk', None))
except TypeError:
cat_id = None
fetch_parent = str2bool(self.request.query_params.get('fetch_parent', 'true'))
queryset = super().get_queryset()
if isinstance(cat_id, int):
params = self.request.query_params
category = params.get('category', None)
if category is not None:
try:
category = PartCategory.objects.get(pk=cat_id)
except PartCategory.DoesNotExist:
# Return empty queryset
return PartCategoryParameterTemplate.objects.none()
category = PartCategory.objects.get(pk=category)
category_list = [cat_id]
fetch_parent = str2bool(params.get('fetch_parent', True))
if fetch_parent:
parent_categories = category.get_ancestors()
for parent in parent_categories:
category_list.append(parent.pk)
if fetch_parent:
parents = category.get_ancestors(include_self=True)
queryset = queryset.filter(category__in=[cat.pk for cat in parents])
else:
queryset = queryset.filter(category=category)
queryset = queryset.filter(category__in=category_list)
except (ValueError, PartCategory.DoesNotExist):
pass
return queryset
@ -341,9 +338,7 @@ class PartThumbs(generics.ListAPIView):
- Images may be used for multiple parts!
"""
queryset = self.get_queryset()
# TODO - We should return the thumbnails here, not the full image!
queryset = self.filter_queryset(self.get_queryset())
# Return the most popular parts first
data = queryset.values(
@ -352,6 +347,19 @@ class PartThumbs(generics.ListAPIView):
return Response(data)
filter_backends = [
filters.SearchFilter,
]
search_fields = [
'name',
'description',
'IPN',
'revision',
'keywords',
'category__name',
]
class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
""" API endpoint for updating Part thumbnails"""
@ -446,6 +454,8 @@ class PartFilter(rest_filters.FilterSet):
else:
queryset = queryset.filter(IPN='')
return queryset
# Regex filter for name
name_regex = rest_filters.CharFilter(label='Filter by name (regex)', field_name='name', lookup_expr='iregex')
@ -995,8 +1005,9 @@ class BomList(generics.ListCreateAPIView):
# Get values for currencies
currencies = queryset.annotate(
purchase_price=F('sub_part__stock_items__purchase_price'),
purchase_price_currency=F('sub_part__stock_items__purchase_price_currency'),
).values('pk', 'sub_part', 'purchase_price_currency')
).values('pk', 'sub_part', 'purchase_price', 'purchase_price_currency')
def convert_price(price, currency, decimal_places=4):
""" Convert price field, returns Money field """
@ -1032,7 +1043,7 @@ class BomList(generics.ListCreateAPIView):
# Find associated currency (select first found)
purchase_price_currency = None
for currency_item in currencies:
if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part.pk:
if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part.pk and currency_item['purchase_price']:
purchase_price_currency = currency_item['purchase_price_currency']
break
# Convert prices
@ -1093,7 +1104,8 @@ part_api_urls = [
# Base URL for PartCategory API endpoints
url(r'^category/', include([
url(r'^(?P<pk>\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
url(r'^parameters/', CategoryParameterList.as_view(), name='api-part-category-parameter-list'),
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
])),

View File

@ -18,7 +18,6 @@ import common.models
from common.forms import MatchItemForm
from .models import Part, PartCategory, PartRelated
from .models import BomItem
from .models import PartParameterTemplate, PartParameter
from .models import PartCategoryParameterTemplate
from .models import PartSellPriceBreak, PartInternalPriceBreak
@ -317,33 +316,6 @@ class EditCategoryParameterTemplateForm(HelperForm):
]
class EditBomItemForm(HelperForm):
""" Form for editing a BomItem object """
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
sub_part = PartModelChoiceField(queryset=Part.objects.all(), label=_('Sub part'))
class Meta:
model = BomItem
fields = [
'part',
'sub_part',
'quantity',
'reference',
'overage',
'note',
'allow_variants',
'inherited',
'optional',
]
# Prevent editing of the part associated with this BomItem
widgets = {
'part': forms.HiddenInput()
}
class PartPriceForm(forms.Form):
""" Simple form for viewing part pricing information """

View File

@ -526,11 +526,14 @@ class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
parameter_template = PartParameterTemplateSerializer(many=False,
read_only=True)
category_detail = CategorySerializer(source='category', many=False, read_only=True)
class Meta:
model = PartCategoryParameterTemplate
fields = [
'pk',
'category',
'category_detail',
'parameter_template',
'default_value',
]

View File

@ -240,32 +240,20 @@
});
$("#cat-create").click(function() {
launchModalForm(
"{% url 'category-create' %}",
{
follow: true,
data: {
{% if category %}
category: {{ category.id }}
{% endif %}
},
secondary: [
{
field: 'default_location',
label: '{% trans "New Location" %}',
title: '{% trans "Create new location" %}',
url: "{% url 'stock-location-create' %}",
},
{
field: 'parent',
label: '{% trans "New Category" %}',
title: '{% trans "Create new category" %}',
url: "{% url 'category-create' %}",
},
]
}
);
})
var fields = categoryFields();
{% if category %}
fields.parent.value = {{ category.pk }};
{% endif %}
constructForm('{% url "api-part-category-list" %}', {
fields: fields,
method: 'POST',
title: '{% trans "Create Part Category" %}',
follow: true,
});
});
$("#part-export").click(function() {
@ -286,12 +274,6 @@
{% endif %}
},
secondary: [
{
field: 'category',
label: '{% trans "New Category" %}',
title: '{% trans "Create new Part Category" %}',
url: "{% url 'category-create' %}",
},
{
field: 'default_location',
label: '{% trans "New Location" %}',
@ -307,24 +289,7 @@
{% if category %}
$("#cat-edit").click(function () {
constructForm(
'{% url "api-part-category-detail" category.pk %}',
{
fields: {
name: {},
description: {},
parent: {
help_text: '{% trans "Select parent category" %}',
},
default_location: {},
default_keywords: {
icon: 'fa-key',
}
},
title: '{% trans "Edit Part Category" %}',
reload: true
}
);
editCategory({{ category.pk }});
});
{% if category.parent %}

View File

@ -416,7 +416,7 @@
});
// Wait for *all* the requests to complete
$.when.apply($, requests).then(function() {
$.when.apply($, requests).done(function() {
location.reload();
});
}
@ -440,22 +440,22 @@
});
$("#bom-item-new").click(function () {
launchModalForm(
"{% url 'bom-item-create' %}?parent={{ part.id }}",
{
success: function() {
$("#bom-table").bootstrapTable('refresh');
},
secondary: [
{
field: 'sub_part',
label: '{% trans "New Part" %}',
title: '{% trans "Create New Part" %}',
url: "{% url 'part-create' %}",
},
]
var fields = bomItemFields();
fields.part.value = {{ part.pk }};
fields.sub_part.filters = {
active: true,
};
constructForm('{% url "api-bom-list" %}', {
fields: fields,
method: 'POST',
title: '{% trans "Create BOM Item" %}',
onSuccess: function() {
$('#bom-table').bootstrapTable('refresh');
}
);
});
});
{% else %}
@ -832,7 +832,7 @@
requests.push(inventreeDelete(url));
});
$.when.apply($, requests).then(function() {
$.when.apply($, requests).done(function() {
reloadSupplierPartTable();
});
}
@ -899,7 +899,7 @@
{% for line in price_history %}'{{ line.date }}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Single Price - {{currency}}{% endblocktrans %}',
label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)',
yAxisID: 'y',
@ -911,7 +911,7 @@
},
{% if 'price_diff' in price_history.0 %}
{
label: '{% blocktrans %}Single Price Difference - {{currency}}{% endblocktrans %}',
label: '{% blocktrans %}Unit Price-Cost Difference - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(68, 157, 68, 0.2)',
borderColor: 'rgb(68, 157, 68)',
yAxisID: 'y2',
@ -923,7 +923,7 @@
hidden: true,
},
{
label: '{% blocktrans %}Part Single Price - {{currency}}{% endblocktrans %}',
label: '{% blocktrans %}Supplier Unit Cost - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(70, 127, 155, 0.2)',
borderColor: 'rgb(70, 127, 155)',
yAxisID: 'y',

View File

@ -10,132 +10,253 @@
{% block content %}
<div class='panel panel-default panel-inventree'>
<div class="panel panel-default panel-inventree">
<!-- Default panel contents -->
<div class="panel-heading"><h3>{{ part.full_name }}</h3></div>
<div class="panel-body">
<div class="row">
<div class="col-sm-6">
{% include "part/part_thumb.html" %}
<div class="media-body">
<p>
<h3>
<!-- Admin View -->
{% if user.is_staff and roles.part.change %}
<a href="{% url 'admin:part_part_change' part.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>&ensp;
{% endif %}
<!-- Properties -->
<div id='part-properties' class='btn-group' role='group'>
{% if part.is_template %}
<span class='fas fa-clone' title='{% trans "Part is a template part (variants can be made from this part)" %}'></span>
{% endif %}
{% if part.assembly %}
<span class='fas fa-tools' title='{% trans "Part can be assembled from other parts" %}'></span>
{% endif %}
{% if part.component %}
<span class='fas fa-th' title='{% trans "Part can be used in assemblies" %}'></span>
{% endif %}
{% if part.trackable %}
<span class='fas fa-directions' title='{% trans "Part stock is tracked by serial number" %}'></span>
{% endif %}
{% if part.purchaseable %}
<span class='fas fa-shopping-cart' title='{% trans "Part can be purchased from external suppliers" %}'></span>
{% endif %}
{% if part.salable %}
<span class='fas fa-dollar-sign' title='{% trans "Part can be sold to customers" %}'></span>
{% endif %}
</div>
<!-- Part active -->
{% if not part.active %}
&ensp;
<div class='label label-large label-large-red'>
<span class='fas fa-skull-crossbones' title='{% trans "Part is virtual (not a physical part)" %}'></span>
{% trans 'Inactive' %}
</div>
{% endif %}
<!-- Part virtual -->
{% if part.virtual and part.active %}
&ensp;
<div class='label label-large label-large-yellow'>
<span class='fas fa-ghost' title='{% trans "Part is virtual (not a physical part)" %}'></span>
{% trans 'Virtual' %}
</div>
{% endif %}
</h3>
</p>
<div class="row">
<div class="col-sm-6">
{% include "part/part_thumb.html" %}
<div class="media-body">
<h3>
{{ part.full_name }}
{% if user.is_staff and roles.part.change %}
<a href="{% url 'admin:part_part_change' part.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %}
{% if not part.active %}
<div class='label label-large label-large-red'>
{% trans 'Inactive' %}
</div>
{% endif %}
</h3>
{% if part.description %}
<p><em>{{ part.description }}</em></p>
{% endif %}
<p>
<div id='part-properties' class='btn-group' role='group'>
{% if part.virtual %}
<span class='fas fa-ghost' title='{% trans "Part is virtual (not a physical part)" %}'></span>
<div class='btn-group action-buttons' role='group'>
<button type='button' class='btn btn-default' id='toggle-starred' title='{% trans "Star this part" %}'>
<span id='part-star-icon' class='fas fa-star {% if starred %}icon-yellow{% endif %}'/>
</button>
{% if barcodes %}
<!-- Barcode actions menu -->
<div class='btn-group'>
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
<ul class='dropdown-menu'>
<li><a href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
</ul>
</div>
{% endif %}
{% if part.is_template %}
<span class='fas fa-clone' title='{% trans "Part is a template part (variants can be made from this part)" %}'></span>
{% endif %}
{% if part.assembly %}
<span class='fas fa-tools' title='{% trans "Part can be assembled from other parts" %}'></span>
{% endif %}
{% if part.component %}
<span class='fas fa-th' title='{% trans "Part can be used in assemblies" %}'></span>
{% endif %}
{% if part.trackable %}
<span class='fas fa-directions' title='{% trans "Part stock is tracked by serial number" %}'></span>
{% if part.active %}
<button type='button' class='btn btn-default' id='price-button' title='{% trans "Show pricing information" %}'>
<span id='part-price-icon' class='fas fa-dollar-sign'/>
</button>
{% if roles.stock.change %}
<div class='btn-group'>
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
<span class='fas fa-boxes'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li>
<a href='#' id='part-count'>
<span class='fas fa-clipboard-list'></span>
{% trans "Count part stock" %}
</a>
</li>
<li>
<a href='#' id='part-move'>
<span class='fas fa-exchange-alt'></span>
{% trans "Transfer part stock" %}
</a>
</li>
</ul>
</div>
{% endif %}
{% if part.purchaseable %}
<span class='fas fa-shopping-cart' title='{% trans "Part can be purchased from external suppliers" %}'></span>
{% endif %}
{% if part.salable %}
<span class='fas fa-dollar-sign' title='{% trans "Part can be sold to customers" %}'></span>
{% endif %}
</div>
</p>
<div class='btn-group action-buttons' role='group'>
<button type='button' class='btn btn-default' id='toggle-starred' title='{% trans "Star this part" %}'>
<span id='part-star-icon' class='fas fa-star {% if starred %}icon-yellow{% endif %}'/>
</button>
{% if barcodes %}
<!-- Barcode actions menu -->
<div class='btn-group'>
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
<ul class='dropdown-menu'>
<li><a href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
</ul>
</div>
{% endif %}
{% if part.active %}
<button type='button' class='btn btn-default' id='price-button' title='{% trans "Show pricing information" %}'>
<span id='part-price-icon' class='fas fa-dollar-sign'/>
</button>
{% if roles.stock.change %}
<div class='btn-group'>
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
<span class='fas fa-boxes'></span> <span class='caret'></span>
{% if roles.purchase_order.add %}
<button type='button' class='btn btn-default' id='part-order' title='{% trans "Order part" %}'>
<span id='part-order-icon' class='fas fa-shopping-cart'/>
</button>
<ul class='dropdown-menu'>
<li>
<a href='#' id='part-count'>
<span class='fas fa-clipboard-list'></span>
{% trans "Count part stock" %}
</a>
</li>
<li>
<a href='#' id='part-move'>
<span class='fas fa-exchange-alt'></span>
{% trans "Transfer part stock" %}
</a>
</li>
</ul>
{% endif %}
{% endif %}
{% endif %}
<!-- Part actions -->
{% if roles.part.add or roles.part.change or roles.part.delete %}
<div class='btn-group'>
<button id='part-actions' title='{% trans "Part actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'> <span class='fas fa-shapes'></span> <span class='caret'></span></button>
<ul class='dropdown-menu'>
{% if roles.part.add %}
<li><a href='#' id='part-duplicate'><span class='fas fa-copy'></span> {% trans "Duplicate part" %}</a></li>
{% endif %}
{% if roles.part.change %}
<li><a href='#' id='part-edit'><span class='fas fa-edit icon-blue'></span> {% trans "Edit part" %}</a></li>
{% endif %}
{% if not part.active and roles.part.delete %}
<li><a href='#' id='part-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete part" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
{% if part.purchaseable %}
{% if roles.purchase_order.add %}
<button type='button' class='btn btn-default' id='part-order' title='{% trans "Order part" %}'>
<span id='part-order-icon' class='fas fa-shopping-cart'/>
</button>
{% endif %}
{% endif %}
{% endif %}
<!-- Part actions -->
{% if roles.part.add or roles.part.change or roles.part.delete %}
<div class='btn-group'>
<button id='part-actions' title='{% trans "Part actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'> <span class='fas fa-shapes'></span> <span class='caret'></span></button>
<ul class='dropdown-menu'>
{% if roles.part.add %}
<li><a href='#' id='part-duplicate'><span class='fas fa-copy'></span> {% trans "Duplicate part" %}</a></li>
{% endif %}
{% if roles.part.change %}
<li><a href='#' id='part-edit'><span class='fas fa-edit icon-blue'></span> {% trans "Edit part" %}</a></li>
{% endif %}
{% if not part.active and roles.part.delete %}
<li><a href='#' id='part-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete part" %}</a></li>
{% endif %}
</ul>
</div>
</div>
<div class='info-messages'>
{% if part.variant_of %}
<div class='alert alert-info alert-block' style='padding: 10px;'>
{% object_link 'part-detail' part.variant_of.id part.variant_of.full_name as link %}
{% blocktrans %}This part is a variant of {{link}}{% endblocktrans %}
</div>
{% endif %}
</div>
<table class='table table-condensed'>
</div>
<div class="col-sm-6">
<table class='table table-condensed table-striped'>
<col width='25'>
<tr>
<td><h4><span class='fas fa-boxes'></span></h4></td>
<td><h4>{% trans "Available Stock" %}</h4></td>
<td><h4>{% decimal available %}{% if part.units %} {{ part.units }}{% endif %}</h4></td>
</tr>
<tr>
<td><span class='fas fa-map-marker-alt'></span></td>
<td>{% trans "In Stock" %}</td>
<td>{% include "part/stock_count.html" %}</td>
</tr>
{% if on_order > 0 %}
<tr>
<td><span class='fas fa-shopping-cart'></span></td>
<td>{% trans "On Order" %}</td>
<td>{% decimal on_order %}</td>
</tr>
{% endif %}
{% if required_build_order_quantity > 0 %}
<tr>
<td><span class='fas fa-clipboard-list'></span></td>
<td>{% trans "Required for Build Orders" %}</td>
<td>{% decimal required_build_order_quantity %}
</tr>
{% endif %}
{% if required_sales_order_quantity > 0 %}
<tr>
<td><span class='fas fa-clipboard-list'></span></td>
<td>{% trans "Required for Sales Orders" %}</td>
<td>{% decimal required_sales_order_quantity %}
</tr>
{% endif %}
{% if allocated > 0 %}
<tr>
<td><span class='fas fa-dolly'></span></td>
<td>{% trans "Allocated to Orders" %}</td>
<td>{% decimal allocated %}</td>
</tr>
{% endif %}
{% if not part.is_template %}
{% if part.assembly %}
<tr>
<td><h4><span class='fas fa-tools'></span></h4></td>
<td colspan='2'>
<h4>{% trans "Build Status" %}</h4>
</td>
</tr>
<tr>
<td></td>
<td>{% trans "Can Build" %}</td>
<td>{% decimal part.can_build %}</td>
</tr>
{% if quantity_being_built > 0 %}
<tr>
<td></td>
<td>{% trans "Building" %}</td>
<td>{% decimal quantity_being_built %}</td>
</tr>
{% endif %}
{% endif %}
{% endif %}
</table>
</div>
</div>
</div>
<p>
<!-- Details show/hide button -->
<button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show">
</button>
</p>
<div class="collapse" id="collapsible-part-details">
<div class="card card-body">
<!-- Details Table -->
<table class="table table-striped">
<col width='25'>
{% if part.IPN %}
<tr>
<td><span class='fas fa-tag'></span></td>
<td>{% trans "IPN" %}</td>
<td>{{ part.IPN }}{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Name" %}</td>
<td>{{ part.name }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Description" %}</td>
<td>{{ part.description }}{% include "clip.html"%}</td>
</tr>
{% if part.revision %}
<tr>
<td><span class='fas fa-code-branch'></span></td>
<td>{% trans "Revision" %}</td>
<td>{{ part.revision }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.keywords %}
<tr>
<td><span class='fas fa-key'></span></td>
<td>{% trans "Keywords" %}</td>
<td>{{ part.keywords }}</td>
<td>{{ part.keywords }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a></td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
@ -154,95 +275,26 @@
<td>{% trans "Latest Serial Number" %}</td>
<td>{{ part.getLatestSerialNumber }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% endif %}
{% if part.default_location %}
<tr>
<td><span class='fas fa-search-location'></span></td>
<td>{% trans "Default Location" %}</td>
<td>{{ part.default_location }}</td>
</tr>
{% endif %}
{% if part.default_supplier %}
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Default Supplier" %}</td>
<td>{{ part.default_supplier }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class='info-messages'>
{% if part.virtual %}
<div class='alert alert-warning alert-block'>
{% trans "This is a virtual part" %}
</div>
{% endif %}
{% if part.variant_of %}
<div class='alert alert-info alert-block'>
{% object_link 'part-detail' part.variant_of.id part.variant_of.full_name as link %}
{% blocktrans %}This part is a variant of {{link}}{% endblocktrans %}
</div>
{% endif %}
</div>
</div>
<div class="col-sm-6">
<table class="table table-striped">
<col width='25'>
<tr>
<td><span class='fas fa-boxes'></span></td>
<td>
<h4>{% trans "Available Stock" %}</h4>
</td>
<td><h4>{% decimal available %}{% if part.units %} {{ part.units }}{% endif %}</h4></td>
</tr>
<tr>
<td><span class='fas fa-map-marker-alt'></span></td>
<td>{% trans "In Stock" %}</td>
<td>{% include "part/stock_count.html" %}</td>
</tr>
{% if on_order > 0 %}
<tr>
<td><span class='fas fa-shopping-cart'></span></td>
<td>{% trans "On Order" %}</td>
<td>{% decimal on_order %}</td>
</tr>
{% endif %}
{% if required_build_order_quantity > 0 %}
<tr>
<td><span class='fas fa-clipboard-list'></span></td>
<td>{% trans "Required for Build Orders" %}</td>
<td>{% decimal required_build_order_quantity %}
</tr>
{% endif %}
{% if required_sales_order_quantity > 0 %}
<tr>
<td><span class='fas fa-clipboard-list'></span></td>
<td>{% trans "Required for Sales Orders" %}</td>
<td>{% decimal required_sales_order_quantity %}
</tr>
{% endif %}
{% if allocated > 0 %}
<tr>
<td><span class='fas fa-dolly'></span></td>
<td>{% trans "Allocated to Orders" %}</td>
<td>{% decimal allocated %}</td>
</tr>
{% endif %}
{% if not part.is_template %}
{% if part.assembly %}
<tr>
<td><span class='fas fa-tools'></span></td>
<td colspan='2'>
<strong>{% trans "Build Status" %}</strong>
</td>
</tr>
<tr>
<td></td>
<td>{% trans "Can Build" %}</td>
<td>{% decimal part.can_build %}</td>
</tr>
{% if quantity_being_built > 0 %}
<tr>
<td></td>
<td>{% trans "Building" %}</td>
<td>{% decimal quantity_being_built %}</td>
</tr>
{% endif %}
{% endif %}
{% endif %}
</table>
</div>
</div>
</div>
{% block page_content %}
@ -363,13 +415,16 @@
// Callback when the image-selection modal form is displayed
// Populate the form with image data (requested via AJAX)
$("#modal-form").find("#image-select-table").bootstrapTable({
pagination: true,
pageSize: 25,
$("#modal-form").find("#image-select-table").inventreeTable({
url: "{% url 'api-part-thumbs' %}",
showHeader: false,
showColumns: false,
clickToSelect: true,
sidePagination: 'server',
singleSelect: true,
formatNoMatches: function() {
return '{% trans "No matching images found" %}';
},
columns: [
{
checkbox: true,
@ -377,6 +432,7 @@
{
field: 'image',
title: 'Image',
searchable: true,
formatter: function(value, row, index, field) {
return "<img src='/media/" + value + "' class='grid-image'/>"
}
@ -394,17 +450,16 @@
{% if roles.part.change %}
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
{% if allow_download %}
$("#part-image-url").click(function() {
launchModalForm(
'{% url "part-image-download" part.id %}',
{
reload: true,
}
);
});
{% endif %}
if (global_settings.INVENTREE_DOWNLOAD_FROM_URL) {
$("#part-image-url").click(function() {
launchModalForm(
'{% url "part-image-download" part.id %}',
{
reload: true,
}
);
});
}
$("#part-image-select").click(function() {
launchModalForm("{% url 'part-image-select' part.id %}",
@ -451,4 +506,42 @@
});
{% endif %}
$("#toggle-part-details").click(function() {
if (this.value == 'show') {
this.innerHTML = '<span class="fas fa-chevron-up"></span> {% trans "Hide Part Details" %}';
this.value = 'hide';
// Store state of part details section
localStorage.setItem("part-details-show", true);
} else {
this.innerHTML = '<span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %}';
this.value = 'show';
// Store state of part details section
localStorage.setItem("part-details-show", false);
}
});
// Load part details section
window.onload = function() {
details_show = localStorage.getItem("part-details-show")
if (details_show === 'true') {
console.log(details_show)
// Get collapsible details section
details = document.getElementById('collapsible-part-details');
// Add "show" class
details.classList.add("in");
// Get toggle
toggle = document.getElementById('toggle-part-details');
// Change state of toggle
toggle.innerHTML = '<span class="fas fa-chevron-up"></span> {% trans "Hide Part Details" %}';
toggle.value = 'hide';
} else {
// Get toggle
toggle = document.getElementById('toggle-part-details');
// Change state of toggle
toggle.innerHTML = '<span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %}';
toggle.value = 'show';
}
}
{% endblock %}

View File

@ -161,7 +161,7 @@
<div class='panel-content'>
<h4>{% trans 'Stock Pricing' %}
<i class="fas fa-info-circle" title="Shows the purchase prices of stock for this part.
The part single price is the current purchase price for that supplier part."></i>
The Supplier Unit Cost is the current purchase price for that supplier part."></i>
</h4>
{% if price_history|length > 0 %}
<div style="max-width: 99%; min-height: 300px">

View File

@ -18,7 +18,7 @@ from InvenTree import version, settings
import InvenTree.helpers
from common.models import InvenTreeSetting, ColorTheme
from common.models import InvenTreeSetting, ColorTheme, InvenTreeUserSetting
from common.settings import currency_code_default
register = template.Library()
@ -69,6 +69,12 @@ def add(x, y, *args, **kwargs):
return x + y
@register.simple_tag()
def to_list(*args):
""" Return the input arguments as list """
return args
@register.simple_tag()
def part_allocation_count(build, part, *args, **kwargs):
""" Return the total number of <part> allocated to <build> """
@ -182,11 +188,12 @@ def setting_object(key, *args, **kwargs):
"""
Return a setting object speciifed by the given key
(Or return None if the setting does not exist)
if a user-setting was requested return that
"""
setting = InvenTreeSetting.get_setting_object(key)
return setting
if 'user' in kwargs:
return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'])
return InvenTreeSetting.get_setting_object(key)
@register.simple_tag()
@ -195,9 +202,29 @@ def settings_value(key, *args, **kwargs):
Return a settings value specified by the given key
"""
if 'user' in kwargs:
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
return InvenTreeSetting.get_setting(key)
@register.simple_tag()
def user_settings(user, *args, **kwargs):
"""
Return all USER settings as a key:value dict
"""
return InvenTreeUserSetting.allValues(user=user)
@register.simple_tag()
def global_settings(*args, **kwargs):
"""
Return all GLOBAL InvenTree settings as a key:value dict
"""
return InvenTreeSetting.allValues()
@register.simple_tag()
def get_color_theme_css(username):
try:
@ -217,6 +244,23 @@ def get_color_theme_css(username):
return inventree_css_static_url
@register.simple_tag()
def get_available_themes(*args, **kwargs):
"""
Return the available theme choices
"""
themes = []
for key, name in ColorTheme.get_color_themes_choices():
themes.append({
'key': key,
'name': name
})
return themes
@register.filter
def keyvalue(dict, key):
"""

View File

@ -243,19 +243,6 @@ class PartQRTest(PartViewTestCase):
class CategoryTest(PartViewTestCase):
""" Tests for PartCategory related views """
def test_create(self):
""" Test view for creating a new category """
response = self.client.get(reverse('category-create'), {'category': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
def test_create_invalid_parent(self):
""" test creation of a new category with an invalid parent """
response = self.client.get(reverse('category-create'), {'category': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Form should still return OK
self.assertEqual(response.status_code, 200)
def test_set_category(self):
""" Test that the "SetCategory" view works """
@ -272,22 +259,3 @@ class CategoryTest(PartViewTestCase):
response = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
class BomItemTests(PartViewTestCase):
""" Tests for BomItem related views """
def test_create_valid_parent(self):
""" Create a BomItem for a valid part """
response = self.client.get(reverse('bom-item-create'), {'parent': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
def test_create_no_parent(self):
""" Create a BomItem without a parent """
response = self.client.get(reverse('bom-item-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
def test_create_invalid_parent(self):
""" Create a BomItem with an invalid parent """
response = self.client.get(reverse('bom-item-create'), {'parent': 99999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)

View File

@ -65,9 +65,6 @@ category_parameter_urls = [
category_urls = [
# Create a new category
url(r'^new/', views.CategoryCreate.as_view(), name='category-create'),
# Top level subcategory display
url(r'^subcategory/', views.PartIndex.as_view(template_name='part/subcategory.html'), name='category-index-subcategory'),
@ -81,10 +78,6 @@ category_urls = [
]))
]
part_bom_urls = [
url(r'^edit/?', views.BomItemEdit.as_view(), name='bom-item-edit'),
]
# URL list for part web interface
part_urls = [
@ -95,9 +88,6 @@ part_urls = [
url(r'^import/', views.PartImport.as_view(), name='part-import'),
url(r'^import-api/', views.PartImportAjax.as_view(), name='api-part-import'),
# Create a new BOM item
url(r'^bom/new/?', views.BomItemCreate.as_view(), name='bom-item-create'),
# Download a BOM upload template
url(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='bom-upload-template'),
@ -125,9 +115,6 @@ part_urls = [
# Change category for multiple parts
url(r'^set-category/?', views.PartSetCategory.as_view(), name='part-set-category'),
# Bom Items
url(r'^bom/(?P<pk>\d+)/', include(part_bom_urls)),
# Individual part using IPN as slug
url(r'^(?P<slug>[-\w]+)/', views.PartDetailFromIPN.as_view(), name='part-detail-from-ipn'),

View File

@ -12,7 +12,7 @@ from django.db.utils import IntegrityError
from django.shortcuts import get_object_or_404
from django.shortcuts import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy
from django.urls import reverse
from django.views.generic import DetailView, ListView
from django.forms.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput
@ -1905,49 +1905,6 @@ class CategoryDelete(AjaxDeleteView):
}
class CategoryCreate(AjaxCreateView):
""" Create view to make a new PartCategory """
model = PartCategory
ajax_form_action = reverse_lazy('category-create')
ajax_form_title = _('Create new part category')
ajax_template_name = 'modal_form.html'
form_class = part_forms.EditCategoryForm
def get_context_data(self, **kwargs):
""" Add extra context data to template.
- If parent category provided, pass the category details to the template
"""
context = super(CategoryCreate, self).get_context_data(**kwargs).copy()
parent_id = self.request.GET.get('category', None)
if parent_id:
try:
context['category'] = PartCategory.objects.get(pk=parent_id)
except PartCategory.DoesNotExist:
pass
return context
def get_initial(self):
""" Get initial data for new PartCategory
- If parent provided, pre-fill the parent category
"""
initials = super(CategoryCreate, self).get_initial().copy()
parent_id = self.request.GET.get('category', None)
if parent_id:
try:
initials['parent'] = PartCategory.objects.get(pk=parent_id)
except PartCategory.DoesNotExist:
pass
return initials
class CategoryParameterTemplateCreate(AjaxCreateView):
""" View for creating a new PartCategoryParameterTemplate """
@ -2121,134 +2078,6 @@ class CategoryParameterTemplateDelete(AjaxDeleteView):
return self.object
class BomItemCreate(AjaxCreateView):
"""
Create view for making a new BomItem object
"""
model = BomItem
form_class = part_forms.EditBomItemForm
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Create BOM Item')
def get_form(self):
""" Override get_form() method to reduce Part selection options.
- Do not allow part to be added to its own BOM
- Remove any Part items that are already in the BOM
"""
form = super(AjaxCreateView, self).get_form()
part_id = form['part'].value()
# Construct a queryset for the part field
part_query = Part.objects.filter(active=True)
# Construct a queryset for the sub_part field
sub_part_query = Part.objects.filter(
component=True,
active=True
)
try:
part = Part.objects.get(id=part_id)
# Hide the 'part' field
form.fields['part'].widget = HiddenInput()
# Exclude the part from its own BOM
sub_part_query = sub_part_query.exclude(id=part.id)
# Eliminate any options that are already in the BOM!
sub_part_query = sub_part_query.exclude(id__in=[item.id for item in part.getRequiredParts()])
except (ValueError, Part.DoesNotExist):
pass
# Set the querysets for the fields
form.fields['part'].queryset = part_query
form.fields['sub_part'].queryset = sub_part_query
return form
def get_initial(self):
""" Provide initial data for the BomItem:
- If 'parent' provided, set the parent part field
"""
# Look for initial values
initials = super(BomItemCreate, self).get_initial().copy()
# Parent part for this item?
parent_id = self.request.GET.get('parent', None)
if parent_id:
try:
initials['part'] = Part.objects.get(pk=parent_id)
except Part.DoesNotExist:
pass
return initials
class BomItemEdit(AjaxUpdateView):
""" Update view for editing BomItem """
model = BomItem
form_class = part_forms.EditBomItemForm
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit BOM item')
def get_form(self):
""" Override get_form() method to filter part selection options
- Do not allow part to be added to its own BOM
- Remove any part items that are already in the BOM
"""
item = self.get_object()
form = super().get_form()
part_id = form['part'].value()
try:
part = Part.objects.get(pk=part_id)
# Construct a queryset
query = Part.objects.filter(component=True)
# Limit to "active" items, *unless* the currently selected item is not active
if item.sub_part.active:
query = query.filter(active=True)
# Prevent the parent part from being selected
query = query.exclude(pk=part_id)
# Eliminate any options that are already in the BOM,
# *except* for the item which is already selected
try:
sub_part_id = int(form['sub_part'].value())
except ValueError:
sub_part_id = -1
existing = [item.pk for item in part.getRequiredParts()]
if sub_part_id in existing:
existing.remove(sub_part_id)
query = query.exclude(id__in=existing)
form.fields['sub_part'].queryset = query
except (ValueError, Part.DoesNotExist):
pass
return form
class PartSalePriceBreakCreate(AjaxCreateView):
"""
View for creating a sale price break for a part

View File

@ -8,7 +8,6 @@ from __future__ import unicode_literals
from django import forms
from django.forms.utils import ErrorDict
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
from mptt.fields import TreeNodeChoiceField
@ -241,14 +240,9 @@ class InstallStockForm(HelperForm):
help_text=_('Stock item to install')
)
quantity_to_install = RoundingDecimalFormField(
max_digits=10, decimal_places=5,
initial=1,
label=_('Quantity'),
help_text=_('Stock quantity to assign'),
validators=[
MinValueValidator(0.001)
]
to_install = forms.BooleanField(
widget=forms.HiddenInput(),
required=False,
)
notes = forms.CharField(
@ -261,7 +255,7 @@ class InstallStockForm(HelperForm):
fields = [
'part',
'stock_item',
'quantity_to_install',
# 'quantity_to_install',
'notes',
]

View File

@ -119,6 +119,11 @@
<h4>{% trans "Installed Stock Items" %}</h4>
</div>
<div class='panel-content'>
<div class='btn-group'>
<button type='button' class='btn btn-success' id='stock-item-install'>
<span class='fas fa-plus-circle'></span> {% trans "Install Stock Item" %}
</button>
</div>
<table class='table table-striped table-condensed' id='installed-table'></table>
</div>
</div>
@ -128,6 +133,20 @@
{% block js_ready %}
{{ block.super }}
$('#stock-item-install').click(function() {
launchModalForm(
"{% url 'stock-item-install' item.pk %}",
{
data: {
'part': {{ item.part.pk }},
'install_item': true,
},
reload: true,
}
);
});
loadInstalledInTable(
$('#installed-table'),
{

View File

@ -127,9 +127,11 @@
<li><a href='#' id='stock-return-from-customer' title='{% trans "Return to stock" %}'><span class='fas fa-undo'></span> {% trans "Return to stock" %}</a></li>
{% endif %}
{% if item.belongs_to %}
<li>
<a href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a>
</li>
<li><a href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a></li>
{% else %}
{% if item.part.get_used_in %}
<li><a href='#' id='stock-install-in' title='{% trans "Install stock item" %}'><span class='fas fa-link'></span> {% trans "Install" %}</a></li>
{% endif %}
{% endif %}
</ul>
</div>
@ -253,7 +255,7 @@
<small>{{ previous.serial }}</small>
</a>
{% endif %}
<span class="btn" href=""><strong>{{ item.serial }}</strong></span>
{{ item.serial }}
{% if next %}
<a class="btn btn-default text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}">
<small>{{ next.serial }}</small>
@ -461,13 +463,27 @@ $("#stock-serialize").click(function() {
);
});
$('#stock-install-in').click(function() {
launchModalForm(
"{% url 'stock-item-install' item.pk %}",
{
data: {
'part': {{ item.part.pk }},
'install_in': true,
},
reload: true,
}
);
});
$('#stock-uninstall').click(function() {
launchModalForm(
"{% url 'stock-item-uninstall' %}",
{
data: {
'items[]': [{{ item.pk}}],
'items[]': [{{ item.pk }}],
},
reload: true,
}

View File

@ -3,15 +3,31 @@
{% block pre_form_content %}
{% if install_item %}
<p>
{% trans "Install another StockItem into this item." %}
{% trans "Install another Stock Item into this item." %}
</p>
<p>
{% trans "Stock items can only be installed if they meet the following criteria" %}:
<ul>
<li>{% trans "The StockItem links to a Part which is in the BOM for this StockItem" %}</li>
<li>{% trans "The StockItem is currently in stock" %}</li>
<li>{% trans "The Stock Item links to a Part which is in the BOM for this Stock Item" %}</li>
<li>{% trans "The Stock Item is currently in stock" %}</li>
<li>{% trans "The Stock Item is serialized and does not belong to another item" %}</li>
</ul>
</p>
{% elif install_in %}
<p>
{% trans "Install this Stock Item in another stock item." %}
</p>
<p>
{% trans "Stock items can only be installed if they meet the following criteria" %}:
<ul>
<li>{% trans "The part associated to this Stock Item belongs to another part's BOM" %}</li>
<li>{% trans "This Stock Item is serialized and does not belong to another item" %}</li>
</ul>
</p>
{% endif %}
{% endblock %}

View File

@ -11,6 +11,7 @@ from django.views.generic import DetailView, ListView, UpdateView
from django.forms.models import model_to_dict
from django.forms import HiddenInput
from django.urls import reverse
from django.http import HttpResponseRedirect
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
@ -91,24 +92,58 @@ class StockItemDetail(InvenTreeRoleMixin, DetailView):
data = super().get_context_data(**kwargs)
if self.object.serialized:
serial_elem = {int(a.serial): a for a in self.object.part.stock_items.all() if a.serialized}
serials = serial_elem.keys()
current = int(self.object.serial)
# previous
for nbr in range(current - 1, -1, -1):
if nbr in serials:
data['previous'] = serial_elem.get(nbr, None)
break
serial_elem = {}
# next
for nbr in range(current + 1, max(serials) + 1):
if nbr in serials:
data['next'] = serial_elem.get(nbr, None)
break
try:
current = int(self.object.serial)
for item in self.object.part.stock_items.all():
if item.serialized:
try:
sn = int(item.serial)
serial_elem[sn] = item
except ValueError:
# We only support integer serial number progression
pass
serials = serial_elem.keys()
# previous
for nbr in range(current - 1, min(serials), -1):
if nbr in serials:
data['previous'] = serial_elem.get(nbr, None)
break
# next
for nbr in range(current + 1, max(serials) + 1):
if nbr in serials:
data['next'] = serial_elem.get(nbr, None)
break
except ValueError:
# We only support integer serial number progression
pass
return data
def get(self, request, *args, **kwargs):
""" check if item exists else return to stock index """
stock_pk = kwargs.get('pk', None)
if stock_pk:
try:
stock_item = StockItem.objects.get(pk=stock_pk)
except StockItem.DoesNotExist:
stock_item = None
if not stock_item:
return HttpResponseRedirect(reverse('stock-index'))
return super().get(request, *args, **kwargs)
class StockItemNotes(InvenTreeRoleMixin, UpdateView):
""" View for editing the 'notes' field of a StockItem object """
@ -501,36 +536,73 @@ class StockItemInstall(AjaxUpdateView):
part = None
def get_params(self):
""" Retrieve GET parameters """
# Look at GET params
self.part_id = self.request.GET.get('part', None)
self.install_in = self.request.GET.get('install_in', False)
self.install_item = self.request.GET.get('install_item', False)
if self.part_id is None:
# Look at POST params
self.part_id = self.request.POST.get('part', None)
try:
self.part = Part.objects.get(pk=self.part_id)
except (ValueError, Part.DoesNotExist):
self.part = None
def get_stock_items(self):
"""
Return a list of stock items suitable for displaying to the user.
Requirements:
- Items must be in stock
Filters:
- Items can be filtered by Part reference
- Items must be in BOM of stock item
- Items must be serialized
"""
# Filter items in stock
items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
# Filter by Part association
# Filter serialized stock items
items = items.exclude(serial__isnull=True).exclude(serial__exact='')
# Look at GET params
part_id = self.request.GET.get('part', None)
if self.part:
# Filter for parts to install this item in
if self.install_in:
# Get parts using this part
allowed_parts = self.part.get_used_in()
# Filter
items = items.filter(part__in=allowed_parts)
if part_id is None:
# Look at POST params
part_id = self.request.POST.get('part', None)
try:
self.part = Part.objects.get(pk=part_id)
items = items.filter(part=self.part)
except (ValueError, Part.DoesNotExist):
self.part = None
# Filter for parts to install in this item
if self.install_item:
# Get parts used in this part's BOM
bom_items = self.part.get_bom_items()
allowed_parts = [item.sub_part for item in bom_items]
# Filter
items = items.filter(part__in=allowed_parts)
return items
def get_context_data(self, **kwargs):
""" Retrieve parameters and update context """
ctx = super().get_context_data(**kwargs)
# Get request parameters
self.get_params()
ctx.update({
'part': self.part,
'install_in': self.install_in,
'install_item': self.install_item,
})
return ctx
def get_initial(self):
initials = super().get_initial()
@ -541,11 +613,16 @@ class StockItemInstall(AjaxUpdateView):
if items.count() == 1:
item = items.first()
initials['stock_item'] = item.pk
initials['quantity_to_install'] = item.quantity
if self.part:
initials['part'] = self.part
try:
# Is this stock item being installed in the other stock item?
initials['to_install'] = self.install_in or not self.install_item
except AttributeError:
pass
return initials
def get_form(self):
@ -558,6 +635,8 @@ class StockItemInstall(AjaxUpdateView):
def post(self, request, *args, **kwargs):
self.get_params()
form = self.get_form()
valid = form.is_valid()
@ -567,13 +646,19 @@ class StockItemInstall(AjaxUpdateView):
data = form.cleaned_data
other_stock_item = data['stock_item']
quantity = data['quantity_to_install']
# Quantity will always be 1 for serialized item
quantity = 1
notes = data['notes']
# Install the other stock item into this one
# Get stock item
this_stock_item = self.get_object()
this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes)
if data['to_install']:
# Install this stock item into the other stock item
other_stock_item.installStockItem(this_stock_item, quantity, request.user, notes)
else:
# Install the other stock item into this one
this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes)
data = {
'form_valid': valid,

View File

@ -93,59 +93,110 @@ function addHeaderAction(label, title, icon, options) {
});
}
{% if roles.part.view %}
{% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %}
{% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %}
{% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %}
{% to_list setting_part_starred setting_part_latest setting_bom_validation as settings_list_part %}
{% if roles.part.view and True in settings_list_part %}
addHeaderTitle('{% trans "Parts" %}');
{% if setting_part_starred %}
addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star');
addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper');
addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle');
loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
params: {
ordering: "-creation_date",
max_results: {% settings_value "PART_RECENT_COUNT" %},
},
name: 'latest_parts',
});
loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", {
params: {
"starred": true,
},
name: 'starred_parts',
});
{% endif %}
{% if setting_part_latest %}
addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper');
loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
params: {
ordering: "-creation_date",
max_results: {% settings_value "PART_RECENT_COUNT" user=request.user %},
},
name: 'latest_parts',
});
{% endif %}
{% if setting_bom_validation %}
addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle');
loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", {
params: {
"bom_valid": false,
},
name: 'bom_invalid_parts',
});
{% endif %}
{% endif %}
{% if roles.stock.view %}
addHeaderTitle('{% trans "Stock" %}');
addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock');
addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart');
addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times');
addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn');
{% settings_value 'HOMEPAGE_STOCK_RECENT' user=request.user as setting_stock_recent %}
{% settings_value 'HOMEPAGE_STOCK_LOW' user=request.user as setting_stock_low %}
{% settings_value 'HOMEPAGE_STOCK_DEPLETED' user=request.user as setting_stock_depleted %}
{% settings_value 'HOMEPAGE_STOCK_NEEDED' user=request.user as setting_stock_needed %}
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
{% settings_value 'HOMEPAGE_STOCK_EXPIRED' user=request.user as setting_stock_expired %}
{% settings_value 'HOMEPAGE_STOCK_STALE' user=request.user as setting_stock_stale %}
{% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed setting_stock_expired setting_stock_stale as settings_list_stock %}
{% else %}
{% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed as settings_list_stock %}
{% endif %}
{% if roles.stock.view and True in settings_list_stock %}
addHeaderTitle('{% trans "Stock" %}');
{% if setting_stock_recent %}
addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock');
loadStockTable($('#table-recently-updated-stock'), {
params: {
part_detail: true,
ordering: "-updated",
max_results: {% settings_value "STOCK_RECENT_COUNT" %},
max_results: {% settings_value "STOCK_RECENT_COUNT" user=request.user %},
},
name: 'recently-updated-stock',
grouping: false,
});
{% endif %}
{% if setting_stock_low %}
addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart');
loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", {
params: {
low_stock: true,
},
name: "low_stock_parts",
});
{% endif %}
{% if setting_stock_depleted %}
addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times');
loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", {
params: {
depleted_stock: true,
},
name: "depleted_stock_parts",
});
{% endif %}
{% if setting_stock_needed %}
addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn');
loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", {
params: {
stock_to_build: true,
},
name: "to_build_parts",
});
{% endif %}
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times');
addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch');
{% if setting_stock_expired %}
addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times');
loadStockTable($("#table-expired-stock"), {
params: {
expired: true,
@ -153,7 +204,10 @@ loadStockTable($("#table-expired-stock"), {
part_detail: true,
},
});
{% endif %}
{% if setting_stock_stale %}
addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch');
loadStockTable($("#table-stale-stock"), {
params: {
stale: true,
@ -164,34 +218,18 @@ loadStockTable($("#table-stale-stock"), {
});
{% endif %}
loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", {
params: {
low_stock: true,
},
name: "low_stock_parts",
});
loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", {
params: {
depleted_stock: true,
},
name: "depleted_stock_parts",
});
loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", {
params: {
stock_to_build: true,
},
name: "to_build_parts",
});
{% endif %}
{% endif %}
{% if roles.build.view %}
addHeaderTitle('{% trans "Build Orders" %}');
addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs');
addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times');
{% settings_value 'HOMEPAGE_BUILD_PENDING' user=request.user as setting_build_pending %}
{% settings_value 'HOMEPAGE_BUILD_OVERDUE' user=request.user as setting_build_overdue %}
{% to_list setting_build_pending setting_build_overdue as settings_list_build %}
{% if roles.build.view and True in settings_list_build %}
addHeaderTitle('{% trans "Build Orders" %}');
{% if setting_build_pending %}
addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs');
loadBuildTable("#table-build-pending", {
url: "{% url 'api-build-list' %}",
params: {
@ -199,7 +237,10 @@ loadBuildTable("#table-build-pending", {
},
disableFilters: true,
});
{% endif %}
{% if setting_build_overdue %}
addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times');
loadBuildTable("#table-build-overdue", {
url: "{% url 'api-build-list' %}",
params: {
@ -209,11 +250,17 @@ loadBuildTable("#table-build-overdue", {
});
{% endif %}
{% if roles.purchase_order.view %}
addHeaderTitle('{% trans "Purchase Orders" %}');
addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt');
addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times');
{% endif %}
{% settings_value 'HOMEPAGE_PO_OUTSTANDING' user=request.user as setting_po_outstanding %}
{% settings_value 'HOMEPAGE_PO_OVERDUE' user=request.user as setting_po_overdue %}
{% to_list setting_po_outstanding setting_po_overdue as settings_list_po %}
{% if roles.purchase_order.view and True in settings_list_po %}
addHeaderTitle('{% trans "Purchase Orders" %}');
{% if setting_po_outstanding %}
addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt');
loadPurchaseOrderTable("#table-po-outstanding", {
url: "{% url 'api-po-list' %}",
params: {
@ -221,7 +268,10 @@ loadPurchaseOrderTable("#table-po-outstanding", {
outstanding: true,
}
});
{% endif %}
{% if setting_po_overdue %}
addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times');
loadPurchaseOrderTable("#table-po-overdue", {
url: "{% url 'api-po-list' %}",
params: {
@ -229,14 +279,19 @@ loadPurchaseOrderTable("#table-po-overdue", {
overdue: true,
}
});
{% endif %}
{% endif %}
{% if roles.sales_order.view %}
addHeaderTitle('{% trans "Sales Orders" %}');
addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt');
addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times');
{% settings_value 'HOMEPAGE_SO_OUTSTANDING' user=request.user as setting_so_outstanding %}
{% settings_value 'HOMEPAGE_SO_OVERDUE' user=request.user as setting_so_overdue %}
{% to_list setting_so_outstanding setting_so_overdue as settings_list_so %}
{% if roles.sales_order.view and True in settings_list_so %}
addHeaderTitle('{% trans "Sales Orders" %}');
{% if setting_so_outstanding %}
addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt');
loadSalesOrderTable("#table-so-outstanding", {
url: "{% url 'api-so-list' %}",
params: {
@ -244,7 +299,10 @@ loadSalesOrderTable("#table-so-outstanding", {
outstanding: true,
},
});
{% endif %}
{% if setting_so_overdue %}
addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times');
loadSalesOrderTable("#table-so-overdue", {
url: "{% url 'api-so-list' %}",
params: {
@ -252,6 +310,7 @@ loadSalesOrderTable("#table-so-overdue", {
customer_detail: true,
}
});
{% endif %}
{% endif %}

View File

@ -1,67 +0,0 @@
{% extends "InvenTree/settings/settings.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='theme' %}
{% endblock %}
{% block subtitle %}
{% trans "Theme Settings" %}
{% endblock %}
{% block settings %}
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Color Themes" %}</h4>
</div>
</div>
<form action="{% url 'settings-appearance' %}" method="post">
{% csrf_token %}
{% load crispy_forms_tags %}
{% crispy form %}
</form>
{% if invalid_color_theme %}
<div class="alert alert-danger alert-block" role="alert" style="display: inline-block;">
{% blocktrans %}
The CSS sheet "{{invalid_color_theme}}.css" for the currently selected color theme was not found.<br>
Please select another color theme :)
{% endblocktrans %}
</div>
{% endif %}
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Language" %}</h4>
</div>
</div>
<div class="row">
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
<input name="next" type="hidden" value="{% url 'settings-appearance' %}">
<div class="col-sm-6" style="width: 200px;"><div id="div_id_name" class="form-group"><div class="controls ">
<select name="language" class="select form-control">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
</div></div></div>
<div class="col-sm-6" style="width: auto;">
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}barcodes{% endblock %}
{% block heading %}
{% trans "Barcode Settings" %}
{% endblock %}
{% block content %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="BARCODE_ENABLE" icon="fa-qrcode" %}
</tbody>
</table>
{% endblock %}

View File

@ -1,16 +1,14 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='build' %}
{% endblock %}
{% block label %}build-order{% endblock %}
{% block subtitle %}
{% block heading %}
{% trans "Build Order Settings" %}
{% endblock %}
{% block settings %}
{% block content %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}

View File

@ -1,114 +1,33 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='category' %}
{% endblock %}
{% block label %}category{% endblock %}
{% block subtitle %}
{% block heading %}
{% trans "Category Settings" %}
{% endblock %}
{% block settings %}
{% block content %}
<form action="{% url 'settings-category' %}" method="post">
{% csrf_token %}
{% load crispy_forms_tags %}
<div id="category-select">
{% crispy form %}
</div>
</form>
<div class='row'>
<form action=''>
<div class='col-sm-6' style='width: 250px'>
<div class='form-group'><div class='controls'>
<select name='category' id='category-select'>
<!-- Will be filled by API -->
</select>
</div></div>
</div>
</form>
</div>
{% if category %}
<hr>
<h4>{% trans "Category Parameter Templates" %}</h4>
<div id='param-buttons'>
<button class='btn btn-success' id='new-param'>
<span class='fas fa-plus-circle'></span> {% trans "New Parameter" %}
<div id='cat-param-buttons'>
<button class='btn btn-success' id='new-cat-param' disabled=''>
<div class='fas fa-plus-circle'></div> {% trans "New Parameter" %}
</button>
</div>
<table class='table table-striped table-condensed' id='param-table' data-toolbar='#param-buttons'>
<table class='table table-striped table-condensed' id='cat-param-table' data-toolbar='#cat-param-buttons'>
</table>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{# Convert dropdown to select2 format #}
$(document).ready(function() {
attachSelect('#category-select');
});
{% if category %}
$("#param-table").inventreeTable({
url: "{% url 'api-part-category-parameters' pk=category.pk %}",
queryParams: {
ordering: 'name',
},
formatNoMatches: function() { return '{% trans "No category parameter templates found" %}'; },
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'parameter_template.name',
title: '{% trans "Parameter Template" %}',
sortable: 'true',
},
{
field: 'default_value',
title: '{% trans "Default Value" %}',
sortable: 'true',
formatter: function(value, row, index, field) {
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 = value
html += "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
return html;
}
}
]
});
$("#new-param").click(function() {
launchModalForm("{% url 'category-param-template-create' category.pk %}", {
success: function() {
$("#param-table").bootstrapTable('refresh');
},
});
});
$("#param-table").on('click', '.template-edit', function() {
var button = $(this);
var url = "/part/category/{{ category.pk }}/parameters/" + button.attr('pk') + "/edit/";
launchModalForm(url, {
success: function() {
$("#param-table").bootstrapTable('refresh');
}
});
});
$("#param-table").on('click', '.template-delete', function() {
var button = $(this);
var url = "/part/category/{{ category.pk }}/parameters/" + button.attr('pk') + "/delete/";
launchModalForm(url, {
success: function() {
$("#param-table").bootstrapTable('refresh');
}
});
});
{% endif %}
{% endblock %}

View File

@ -1,16 +1,14 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='currencies' %}
{% endblock %}
{% block label %}currencies{% endblock %}
{% block subtitle %}
{% block heading %}
{% trans "Currency Settings" %}
{% endblock %}
{% block settings %}
{% block content %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
@ -55,8 +53,4 @@
</tbody>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% endblock %}

View File

@ -1,16 +1,15 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='global' %}
{% block label %}server{% endblock %}
{% block heading %}
{% trans "Server Settings" %}
{% endblock %}
{% block subtitle %}
{% trans "Global InvenTree Settings" %}
{% endblock %}
{% block settings %}
{% block content %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
@ -23,19 +22,4 @@
</tbody>
</table>
<h4>{% trans "Barcode Settings" %}</h4>
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="BARCODE_ENABLE" icon="fa-qrcode" %}
</tbody>
</table>
<h4>{% trans "Search Settings" %}</h4>
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" icon="fa-search" %}
</tbody>
</table>
{% endblock %}

View File

@ -0,0 +1,109 @@
{% load i18n %}
<ul class='list-group'>
<li class='list-group-item'>
<a href='#' id='item-menu-toggle'>
<span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
</a>
</li>
<li class='list-group-item'>
<b>{% trans "User Settings" %}</b>
</li>
<li class='list-group-item' title='{% trans "Account" %}'>
<a href='#' class='nav-toggle' id='select-account'>
<span class='fas fa-user'></span> {% trans "Account" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Home Page" %}'>
<a href='#' class='nav-toggle' id='select-user-home'>
<span class='fas fa-home'></span> {% trans "Home Page" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Search" %}'>
<a href='#' class='nav-toggle' id='select-user-search'>
<span class='fas fa-search'></span> {% trans "Search" %}
</a>
</li>
<!--
<li class='list-group-item' title='{% trans "Settings" %}'>
<a href='#' class='nav-toggle' id='select-user-settings'>
<span class='fas fa-cog'></span> {% trans "Settings" %}
</a>
</li>
-->
{% if user.is_staff %}
<li class='list-group-item'>
<b>{% trans "InvenTree Settings" %}</b>
</li>
<li class='list-group-item' title='{% trans "Server" %}'>
<a href='#' class='nav-toggle' id='select-server'>
<span class='fas fa-server'></span> {% trans "Server" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Barcodes" %}'>
<a href='#' class='nav-toggle' id='select-barcodes'>
<span class='fas fa-qrcode'></span> {% trans "Barcodes" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Currencies" %}'>
<a href='#' class='nav-toggle' id='select-currencies'>
<span class='fas fa-dollar-sign'></span> {% trans "Currencies" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Reporting" %}'>
<a href='#' class='nav-toggle' id='select-reporting'>
<span class='fas fa-file-pdf'></span> {% trans "Reporting" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Parts" %}'>
<a href='#' class='nav-toggle' id='select-parts'>
<span class='fas fa-shapes'></span> {% trans "Parts" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Categories" %}'>
<a href='#' class='nav-toggle' id='select-category'>
<span class='fas fa-shapes'></span> {% trans "Categories" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Stock" %}'>
<a href='#' class='nav-toggle' id='select-stock'>
<span class='fas fa-boxes'></span> {% trans "Stock" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Build Orders" %}'>
<a href='#' class='nav-toggle' id='select-build-order'>
<span class='fas fa-tools'></span> {% trans "Build Orders" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Purchase Orders" %}'>
<a href='#' class='nav-toggle' id='select-purchase-order'>
<span class='fas fa-shopping-cart'></span> {% trans "Purchase Orders" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Sales Orders" %}'>
<a href='#' class='nav-toggle' id='select-sales-order'>
<span class='fas fa-truck'></span> {% trans "Sales Orders" %}
</a>
</li>
{% endif %}
</ul>

View File

@ -1,15 +1,13 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='part' %}
{% endblock %}
{% block label %}parts{% endblock %}
{% block subtitle %}
{% block heading %}
{% trans "Part Settings" %}
{% endblock %}
{% block settings %}
{% block content %}
<h4>{% trans "Part Options" %}</h4>
@ -22,7 +20,6 @@
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %}
{% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" %}
{% include "InvenTree/settings/setting.html" with key="PART_CREATE_INITIAL" icon="fa-boxes" %}
<tr><td colspan='5'></td></tr>
{% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %}

View File

@ -1,15 +1,13 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='po' %}
{% endblock %}
{% block label %}purchase-order{% endblock %}
{% block subtitle %}
{% block heading %}
{% trans "Purchase Order Settings" %}
{% endblock %}
{% block settings %}
{% block content %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>

View File

@ -1,16 +1,14 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='report' %}
{% endblock %}
{% block label %}reporting{% endblock %}
{% block subtitle %}
{% block heading %}
{% trans "Report Settings" %}
{% endblock %}
{% block settings %}
{% block content %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}

View File

@ -1,7 +1,12 @@
{% load inventree_extras %}
{% load i18n %}
{% setting_object key as setting %}
{% if user_setting %}
{% setting_object key user=request.user as setting %}
{% else %}
{% setting_object key as setting %}
{% endif %}
<tr>
<td>
{% if icon %}
@ -12,23 +17,28 @@
<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 %}>
<input fieldname='{{ setting.key.upper }}' id='setting-value-{{ setting.key.upper }}' type='checkbox' disabled='' {% if setting.as_bool %}checked=''{% endif %}>
</div>
{% else %}
{% if setting.value %}
<i><b>
{{ setting.value }}</b> {{ setting.units }}
</i>
{% else %}
<i>{% trans "No value set" %}</i>
{% endif %}
<div id='setting-{{ setting.pk }}'>
<b>
<span id='setting-value-{{ setting.key.upper }}' fieldname='{{ setting.key.upper }}'>
{% if setting.value %}
{{ setting.value }}
{% else %}
<i>{% trans "No value set" %}</i>
{% endif %}
</span>
</b>
{{ setting.units }}
</div>
{% endif %}
<td>
{% trans 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" %}'>
<button class='btn btn-default btn-glyph btn-edit-setting' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' title='{% trans "Edit setting" %}' {% if user_setting %}user='{{request.user.id}}'{% endif %}>
<span class='fas fa-edit icon-green'></span>
</button>
</div>

View File

@ -8,30 +8,31 @@
{% inventree_title %} | {% trans "Settings" %}
{% endblock %}
{% block menubar %}
{% include "InvenTree/settings/navbar.html" %}
{% endblock %}
{% block content %}
<div class='settings-container'>
<h3>InvenTree {% trans "Settings" %}</h3>
<hr>
{% include "InvenTree/settings/user.html" %}
{% include "InvenTree/settings/user_settings.html" %}
{% include "InvenTree/settings/user_homepage.html" %}
{% include "InvenTree/settings/user_search.html" %}
<div class='settings-nav'>
{% block tabs %}
{% include "InvenTree/settings/tabs.html" %}
{% endblock %}
</div>
{% if user.is_staff %}
<div class='settings-content'>
<h3>
{% block subtitle %}
SUBTITLE GOES HERE
{% endblock %}
</h3>
<hr>
{% block settings %}
{% endblock %}
</div>
{% include "InvenTree/settings/global.html" %}
{% include "InvenTree/settings/barcode.html" %}
{% include "InvenTree/settings/currencies.html" %}
{% include "InvenTree/settings/report.html" %}
{% include "InvenTree/settings/part.html" %}
{% include "InvenTree/settings/category.html" %}
{% include "InvenTree/settings/stock.html" %}
{% include "InvenTree/settings/build.html" %}
{% include "InvenTree/settings/po.html" %}
{% include "InvenTree/settings/so.html" %}
</div>
{% endif %}
{% endblock %}
@ -45,13 +46,209 @@
$('table').find('.btn-edit-setting').click(function() {
var setting = $(this).attr('setting');
var pk = $(this).attr('pk');
var url = `/settings/${pk}/edit/`;
if ($(this).attr('user')){
url += `user/`;
}
launchModalForm(
`/settings/${pk}/edit/`,
url,
{
success: function(response) {
if (response.is_bool) {
var enabled = response.value.toLowerCase() == 'true';
$(`#setting-value-${setting}`).prop('checked', enabled);
} else {
$(`#setting-value-${setting}`).html(response.value);
}
}
}
);
});
$("#edit-user").on('click', function() {
launchModalForm(
"{% url 'edit-user' %}",
{
reload: true,
}
);
});
$("#edit-password").on('click', function() {
launchModalForm(
"{% url 'set-password' %}",
{
reload: true,
}
);
});
$('#category-select').select2({
placeholder: '',
width: '100%',
ajax: {
url: '{% url "api-part-category-list" %}',
dataType: 'json',
delay: 250,
cache: false,
data: function(params) {
if (!params.page) {
offset = 0;
} else {
offset = (params.page - 1) * 25;
}
return {
search: params.term,
offset: offset,
limit: 25,
};
},
processResults: function(response) {
var data = [];
var more = false;
if ('count' in response && 'results' in response) {
// Response is paginated
data = response.results;
// Any more data available?
if (response.next) {
more = true;
}
} else {
// Non-paginated response
data = response;
}
// Each 'row' must have the 'id' attribute
for (var idx = 0; idx < data.length; idx++) {
data[idx].id = data[idx].pk;
data[idx].text = data[idx].pathstring;
}
// Ref: https://select2.org/data-sources/formats
var results = {
results: data,
pagination: {
more: more,
}
};
return results;
}
},
});
$('#cat-param-table').inventreeTable({
formatNoMatches: function() { return '{% trans "No category parameter templates found" %}'; },
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'parameter_template.name',
title: '{% trans "Parameter Template" %}',
sortable: 'true',
},
{
field: 'category_detail.pathstring',
title: '{% trans "Category" %}',
},
{
field: 'default_value',
title: '{% trans "Default Value" %}',
sortable: 'true',
formatter: function(value, row, index, field) {
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 = value
html += "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
return html;
}
}
]
});
function loadTemplateTable(pk) {
console.log('refresh:', pk);
// Enable the buttons
$('#new-cat-param').removeAttr('disabled');
// Load the parameter table
$("#cat-param-table").bootstrapTable('refresh', {
query: {
category: pk,
},
url: '{% url "api-part-category-parameter-list" %}',
});
}
$('body').on('change', '#category-select', function() {
var pk = $(this).val();
loadTemplateTable(pk);
});
$("#new-cat-param").click(function() {
var pk = $('#category-select').val();
launchModalForm(`/part/category/${pk}/parameters/new/`, {
success: function() {
$("#cat-param-table").bootstrapTable('refresh');
},
});
});
$("#cat-param-table").on('click', '.template-edit', function() {
var category = $('#category-select').val();
var pk = $(this).attr('pk');
var url = `/part/category/${category}/parameters/${pk}/edit/`;
launchModalForm(url, {
success: function() {
$("#cat-param-table").bootstrapTable('refresh');
}
});
});
$("#cat-param-table").on('click', '.template-delete', function() {
var category = $('#category-select').val();
var pk = $(this).attr('pk');
var url = `/part/category/${category}/parameters/${pk}/delete/`;
launchModalForm(url, {
success: function() {
$("#cat-param-table").bootstrapTable('refresh');
}
});
});
enableNavbar({
label: 'settings',
toggleId: '#item-menu-toggle',
});
attachNavCallbacks({
name: 'settings',
default: 'account'
});
{% endblock %}

View File

@ -1,15 +1,13 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='so' %}
{% endblock %}
{% block label %}sales-order{% endblock %}
{% block subtitle %}
{% block heading %}
{% trans "Sales Order Settings" %}
{% endblock %}
{% block settings %}
{% block content %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}

View File

@ -1,22 +1,18 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='stock' %}
{% endblock %}
{% block label %}stock{% endblock %}
{% block subtitle %}
{% block heading %}
{% trans "Stock Settings" %}
{% endblock %}
{% block settings %}
<h4>{% trans "Stock Options" %}</h4>
{% block content %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="STOCK_GROUP_BY_PART" icon="fa-layer-group" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_RECENT_COUNT" icon="fa-clock" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_ENABLE_EXPIRY" icon="fa-stopwatch" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_STALE_DAYS" icon="fa-calendar" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" icon="fa-truck" %}

View File

@ -1,43 +0,0 @@
{% load i18n %}
<h4><span class='fas fa-user'></span> {% trans "User Settings" %}</h4>
<ul class='nav nav-pills nav-stacked'>
<li{% ifequal tab 'user' %} class='active'{% endifequal %}>
<a href="{% url 'settings-user' %}"><span class='fas fa-user'></span> {% trans "Account" %}</a>
</li>
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
<a href="{% url 'settings-appearance' %}"><span class='fas fa-fill'></span> {% trans "Appearance" %}</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 {% if tab == 'currencies' %} class='active'{% endif %}>
<a href="{% url 'settings-currencies' %}"><span class='fas fa-dollar-sign'></span> {% trans "Currencies" %}</a>
</li>
<li {% if tab == 'report' %} class='active' {% endif %}>
<a href='{% url "settings-report" %}'><span class='fas fa-file-pdf'></span> {% trans "Report" %}</a>
</li>
<li{% ifequal tab 'category' %} class='active'{% endifequal %}>
<a href="{% url 'settings-category' %}"><span class='fa fa-sitemap'></span> {% trans "Categories" %}</a>
</li>
<li{% ifequal tab 'part' %} class='active'{% endifequal %}>
<a href="{% url 'settings-part' %}"><span class='fas fa-shapes'></span> {% trans "Parts" %}</a>
</li>
<li {% if tab == 'stock' %} class='active'{% endif %}>
<a href='{% url 'settings-stock' %}'><span class='fas fa-boxes'></span> {% trans "Stock" %}</a>
</li>
<li {% if tab == 'build' %} class='active'{% endif %}>
<a href="{% url 'settings-build' %}"><span class='fas fa-tools'></span> {% trans "Build Orders" %}</a>
</li>
<li {% if tab == 'po' %} class='active'{% endif %}>
<a href="{% url 'settings-po' %}"><span class='fas fa-shopping-cart'></span> {% trans "Purchase Orders" %}</a>
</li>
<li {% if tab == 'so' %} class='active'{% endif %}>
<a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
</li>
</ul>
{% endif %}

View File

@ -1,69 +1,95 @@
{% extends "InvenTree/settings/settings.html" %}
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='user' %}
{% block label %}account{% endblock %}
{% block heading %}
{% trans "Account Settings" %}
{% endblock %}
{% block subtitle %}
{% trans "User Settings" %}
{% endblock %}
{% block settings %}
<div class='container'>
<h4>{% trans "User Information" %}</h4>
<div class='btn-group' style='float: right;'>
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
</div>
<div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
<span class='fas fa-key'></span> {% trans "Set Password" %}
</div>
{% block content %}
<div class='btn-group' style='float: right;'>
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
</div>
<div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
<span class='fas fa-key'></span> {% trans "Set Password" %}
</div>
<table class='table table-striped table-condensed'>
<tr>
<td>{% trans "Username" %}</td>
<td>{{ user.username }}</td>
</tr>
<tr>
<td>{% trans "First Name" %}</td>
<td>{{ user.first_name }}</td>
</tr>
<tr>
<td>{% trans "Last Name" %}</td>
<td>{{ user.last_name }}</td>
</tr>
<tr>
<td>{% trans "Email Address" %}</td>
<td>{{ user.email }}</td>
</tr>
</table>
</div>
{% endblock %}
<table class='table table-striped table-condensed'>
<tr>
<td>{% trans "Username" %}</td>
<td>{{ user.username }}</td>
</tr>
<tr>
<td>{% trans "First Name" %}</td>
<td>{{ user.first_name }}</td>
</tr>
<tr>
<td>{% trans "Last Name" %}</td>
<td>{{ user.last_name }}</td>
</tr>
<tr>
<td>{% trans "Email Address" %}</td>
<td>{{ user.email }}</td>
</tr>
</table>
{% block js_ready %}
{{ block.super }}
<div class='panel-heading'>
<h4>{% trans "Theme Settings" %}</h4>
</div>
$("#edit-user").on('click', function() {
launchModalForm(
"{% url 'edit-user' %}",
{
reload: true,
}
);
});
<div class='row'>
$("#edit-password").on('click', function() {
launchModalForm(
"{% url 'set-password' %}",
{
reload: true,
}
);
});
<form action='{% url "settings-appearance" %}' method='post'>
{% csrf_token %}
<input name='next' type='hidden' value='{% url "settings" %}'>
<div class="col-sm-6" style="width: 200px;">
<div id="div_id_themes" class="form-group">
<div class="controls ">
<select name='theme' class='select form-control'>
{% get_available_themes as themes %}
{% for theme in themes %}
<option value='{{ theme.key }}'>{{ theme.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="col-sm-6" style="width: auto;">
<input type="submit" value="{% trans 'Set Theme' %}" class="btn btn btn-primary">
</div>
</form>
</div>
<div class='panel-heading'>
<h4>{% trans "Language Settings" %}</h4>
</div>
<div class="row">
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<input name="next" type="hidden" value="{% url 'settings' %}">
<div class="col-sm-6" style="width: 200px;"><div id="div_id_language" class="form-group"><div class="controls ">
<select name="language" class="select form-control">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
</div></div></div>
<div class="col-sm-6" style="width: auto;">
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}user-home{% endblock %}
{% block heading %}
{% trans "Home Page Settings" %}
{% endblock %}
{% block content %}
<div class='row'>
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" icon='fa-star' user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" icon='fa-history' user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %}
<tr><td colspan='5'></td></tr>
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_RECENT" icon='fa-history' user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="STOCK_RECENT_COUNT" icon="fa-clock" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_LOW" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_DEPLETED" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_NEEDED" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_EXPIRED" icon='fa-calendar-alt' user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_STALE" icon='fa-calendar-alt' user_setting=True %}
<tr><td colspan='5'></td></tr>
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BUILD_PENDING" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BUILD_OVERDUE" user_setting=True %}
<tr><td colspan='5'></td></tr>
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PO_OUTSTANDING" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PO_OVERDUE" user_setting=True %}
<tr><td colspan='5'></td></tr>
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_SO_OUTSTANDING" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_SO_OVERDUE" user_setting=True %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}user-search{% endblock %}
{% block heading %}
{% trans "Search Settings" %}
{% endblock %}
{% block content %}
<div class='row'>
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" user_setting=True icon='fa-search' %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}user-settings{% endblock %}
{% block heading %}
{% trans "User Settings" %}
{% endblock %}
{% block content %}
<div class='row'>
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
</table>
</div>
{% endblock %}

View File

@ -145,19 +145,22 @@
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
<!-- translated -->
<script type='text/javascript' src="{% i18n_static 'inventree.js' %}"></script>
<!-- dynamic javascript templates -->
<script type='text/javascript' src="{% url 'inventree.js' %}"></script>
<script type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script type='text/javascript' src="{% url 'nav.js' %}"></script>
<script type='text/javascript' src="{% url 'settings.js' %}"></script>
<!-- translated javascript templates-->
<script type='text/javascript' src="{% i18n_static 'api.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'bom.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'build.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'calendar.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'company.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'filters.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'forms.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'label.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'nav.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'modals.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'model_renderers.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'order.js' %}"></script>

View File

@ -91,7 +91,7 @@ function inventreeDocReady() {
url: '/api/part/',
data: {
search: request.term,
limit: {% settings_value 'SEARCH_PREVIEW_RESULTS' %},
limit: user_settings.SEARCH_PREVIEW_RESULTS,
offset: 0
},
success: function (data) {

View File

@ -0,0 +1,17 @@
{% load inventree_extras %}
// InvenTree settings
{% user_settings request.user as USER_SETTINGS %}
{% global_settings as GLOBAL_SETTINGS %}
var user_settings = {
{% for setting in USER_SETTINGS %}
{{ setting.key }}: {{ setting.value }},
{% endfor %}
};
var global_settings = {
{% for setting in GLOBAL_SETTINGS %}
{{ setting.key }}: {{ setting.value }},
{% endfor %}
};

View File

@ -147,8 +147,7 @@ function inventreeDelete(url, options={}) {
options.method = 'DELETE';
inventreePut(url, {}, options);
return inventreePut(url, {}, options);
}

View File

@ -8,6 +8,26 @@
*/
function bomItemFields() {
return {
part: {
hidden: true,
},
sub_part: {
},
quantity: {},
reference: {},
overage: {},
note: {},
allow_variants: {},
inherited: {},
optional: {},
};
}
function reloadBomTable(table, options) {
table.bootstrapTable('refresh');
@ -262,13 +282,13 @@ function loadBomTable(table, options) {
cols.push(
{
field: 'price_range',
title: '{% trans "Buy Price" %}',
title: '{% trans "Supplier Cost" %}',
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
return value;
} else {
return "<span class='warning-msg'>{% trans 'No pricing available' %}</span>";
return "<span class='warning-msg'>{% trans 'No supplier pricing available' %}</span>";
}
}
});
@ -528,14 +548,15 @@ function loadBomTable(table, options) {
var pk = $(this).attr('pk');
var url = `/part/bom/${pk}/edit/`;
launchModalForm(
url,
{
success: function() {
reloadBomTable(table);
}
var fields = bomItemFields();
constructForm(`/api/bom/${pk}/`, {
fields: fields,
title: '{% trans "Edit BOM Item" %}',
onSuccess: function() {
reloadBomTable(table);
}
);
});
});
table.on('click', '.bom-validate-button', function() {

View File

@ -5,7 +5,7 @@
function buildFormFields() {
return {
reference: {
prefix: "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.BUILDORDER_REFERENCE_PREFIX,
},
title: {},
part: {},
@ -232,7 +232,7 @@ function loadBuildOrderAllocationTable(table, options={}) {
switchable: false,
title: '{% trans "Build Order" %}',
formatter: function(value, row) {
var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.build_detail.reference}`;
@ -848,7 +848,7 @@ function loadBuildTable(table, options) {
switchable: true,
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;

View File

@ -349,7 +349,7 @@ function deleteManufacturerParts(selections, options={}) {
});
// Wait for all the requests to complete
$.when.apply($, requests).then(function() {
$.when.apply($, requests).done(function() {
if (options.onSuccess) {
options.onSuccess();

View File

@ -265,6 +265,8 @@ function setupFilterList(tableKey, table, target) {
// One blank slate, please
element.empty();
element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-default filter-tag'><span class='fas fa-redo-alt'></span></button>`);
element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-default filter-tag'><span class='fas fa-filter'></span></button>`);
if (Object.keys(filters).length > 0) {
@ -279,6 +281,11 @@ function setupFilterList(tableKey, table, target) {
element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
}
// Callback for reloading the table
element.find(`#reload-${tableKey}`).click(function() {
$(table).bootstrapTable('refresh');
});
// Add a callback for adding a new filter
element.find(`#${add}`).click(function clicked() {

View File

@ -9,7 +9,7 @@ function createSalesOrder(options={}) {
method: 'POST',
fields: {
reference: {
prefix: '{% settings_value "SALESORDER_REFERENCE_PREFIX" %}',
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
},
customer: {
value: options.customer,
@ -40,7 +40,7 @@ function createPurchaseOrder(options={}) {
method: 'POST',
fields: {
reference: {
prefix: "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
},
supplier: {
value: options.supplier,
@ -214,7 +214,7 @@ function loadPurchaseOrderTable(table, options) {
switchable: false,
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
@ -309,7 +309,7 @@ function loadSalesOrderTable(table, options) {
title: '{% trans "Sales Order" %}',
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
@ -423,7 +423,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
switchable: false,
formatter: function(value, row) {
var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.order_detail.reference}`;

View File

@ -14,6 +14,37 @@ function yesNoLabel(value) {
}
function categoryFields() {
return {
parent: {
help_text: '{% trans "Parent part category" %}',
},
name: {},
description: {},
default_location: {},
default_keywords: {
icon: 'fa-key',
}
};
}
// Edit a PartCategory via the API
function editCategory(pk, options={}) {
var url = `/api/part/category/${pk}/`;
var fields = categoryFields();
constructForm(url, {
fields: fields,
title: '{% trans "Edit Part Category" %}',
reload: true,
});
}
function editPart(pk, options={}) {
var url = `/api/part/${pk}/`;

View File

@ -6,8 +6,6 @@
* Requires api.js to be loaded first
*/
{% settings_value 'BARCODE_ENABLE' as barcodes %}
function stockStatusCodes() {
return [
{% for code in StockStatus.list %}
@ -287,7 +285,7 @@ function adjustStock(action, items, options={}) {
});
// Wait for *all* the requests to complete
$.when.apply($, requests).then(function() {
$.when.apply($, requests).done(function() {
// Destroy the modal window
$(modal).modal('hide');
@ -704,8 +702,7 @@ function loadStockTable(table, options) {
name: 'stock',
original: original,
showColumns: true,
{% settings_value 'STOCK_GROUP_BY_PART' as group_by_part %}
{% if group_by_part %}
{% if False %}
groupByField: options.groupByField || 'part',
groupBy: grouping,
groupByFormatter: function(field, id, data) {
@ -1011,14 +1008,13 @@ function loadStockTable(table, options) {
title: '{% trans "Stocktake" %}',
sortable: true,
},
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
{
field: 'expiry_date',
title: '{% trans "Expiry Date" %}',
sortable: true,
visible: global_settings.STOCK_ENABLE_EXPIRY,
switchable: global_settings.STOCK_ENABLE_EXPIRY,
},
{% endif %}
{
field: 'updated',
title: '{% trans "Last Updated" %}',
@ -1037,7 +1033,7 @@ function loadStockTable(table, options) {
if (row.purchase_order_reference) {
var prefix = '{% settings_value "PURCHASEORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
text = prefix + row.purchase_order_reference;
}
@ -1090,15 +1086,18 @@ function loadStockTable(table, options) {
}
*/
var buttons = [
'#stock-print-options',
'#stock-options',
];
if (global_settings.BARCODE_ENABLE) {
buttons.push('#stock-barcode-options');
}
linkButtonsToSelection(
table,
[
'#stock-print-options',
{% if barcodes %}
'#stock-barcode-options',
{% endif %}
'#stock-options',
]
buttons,
);
@ -1138,19 +1137,19 @@ function loadStockTable(table, options) {
printTestReports(items);
})
{% if barcodes %}
$('#multi-item-barcode-scan-into-location').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
if (global_settings.BARCODE_ENABLE) {
$('#multi-item-barcode-scan-into-location').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
var items = [];
var items = [];
selections.forEach(function(item) {
items.push(item.pk);
})
selections.forEach(function(item) {
items.push(item.pk);
})
scanItemsIntoLocation(items);
});
{% endif %}
scanItemsIntoLocation(items);
});
}
$('#multi-item-stocktake').click(function() {
stockAdjustment('count');
@ -1265,7 +1264,7 @@ function loadStockTable(table, options) {
);
});
$.when.apply($, requests).then(function() {
$.when.apply($, requests).done(function() {
$("#stock-table").bootstrapTable('refresh');
});
})

View File

@ -121,7 +121,8 @@ function getAvailableTableFilters(tableKey) {
// Filters for the "Stock" table
if (tableKey == 'stock') {
return {
var filters = {
active: {
type: 'bool',
title: '{% trans "Active parts" %}',
@ -147,19 +148,6 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Depleted" %}',
description: '{% trans "Show stock items which are depleted" %}',
},
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
expired: {
type: 'bool',
title: '{% trans "Expired" %}',
description: '{% trans "Show stock items which have expired" %}',
},
stale: {
type: 'bool',
title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}',
},
{% endif %}
in_stock: {
type: 'bool',
title: '{% trans "In Stock" %}',
@ -216,6 +204,23 @@ function getAvailableTableFilters(tableKey) {
description: '{% trans "Show stock items which have a purchase price set" %}',
},
};
// Optional filters if stock expiry functionality is enabled
if (global_settings.STOCK_ENABLE_EXPIRY) {
filters.expired = {
type: 'bool',
title: '{% trans "Expired" %}',
description: '{% trans "Show stock items which have expired" %}',
};
filters.stale = {
type: 'bool',
title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}',
};
}
return filters;
}
// Filters for the 'stock test' table

Some files were not shown because too many files have changed in this diff Show More