mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-18 21:00:30 -06:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57eada1da1 | ||
|
|
f526dcdeec | ||
|
|
aacf35ed47 | ||
|
|
ca986cba01 | ||
|
|
699fb83dd4 | ||
|
|
dd6e225cda | ||
|
|
1f3a49b1ae | ||
|
|
385e7cb478 | ||
|
|
73768bfee1 | ||
|
|
946fe2df29 | ||
|
|
afa7ed873f | ||
|
|
46da332afe | ||
|
|
072b7b3146 | ||
|
|
1d51b2a058 | ||
|
|
08f9bebdf0 | ||
|
|
6d6629f11c | ||
|
|
db88fbda11 | ||
|
|
49c9b5b1aa | ||
|
|
e1a0e79ead | ||
|
|
ab22f2a04d | ||
|
|
8a58bf5ffa | ||
|
|
6730098bac | ||
|
|
93b44ad8e6 | ||
|
|
9b5e828b87 | ||
|
|
cf5d637678 | ||
|
|
feb2acf668 | ||
|
|
0017570dd3 | ||
|
|
4c41a50bb1 | ||
|
|
eab3fdcf2c | ||
|
|
c59eee7359 | ||
|
|
4a5ebf8f01 | ||
|
|
698798fee7 | ||
|
|
2660889879 | ||
|
|
01aaf95a0e |
@@ -31,3 +31,15 @@ class InvenTreeResource(ModelResource):
|
|||||||
row[idx] = val
|
row[idx] = val
|
||||||
|
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
def get_fields(self, **kwargs):
|
||||||
|
"""Return fields, with some common exclusions"""
|
||||||
|
|
||||||
|
fields = super().get_fields(**kwargs)
|
||||||
|
|
||||||
|
fields_to_exclude = [
|
||||||
|
'metadata',
|
||||||
|
'lft', 'rght', 'tree_id', 'level',
|
||||||
|
]
|
||||||
|
|
||||||
|
return [f for f in fields if f.column_name not in fields_to_exclude]
|
||||||
|
|||||||
@@ -59,14 +59,39 @@ class NotFoundView(AjaxView):
|
|||||||
|
|
||||||
permission_classes = [permissions.AllowAny]
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def not_found(self, request):
|
||||||
"""Process an `not found` event on the API."""
|
"""Return a 404 error"""
|
||||||
data = {
|
return JsonResponse(
|
||||||
'details': _('API endpoint not found'),
|
{
|
||||||
'url': request.build_absolute_uri(),
|
'detail': _('API endpoint not found'),
|
||||||
}
|
'url': request.build_absolute_uri(),
|
||||||
|
},
|
||||||
|
status=404
|
||||||
|
)
|
||||||
|
|
||||||
return JsonResponse(data, status=404)
|
def options(self, request, *args, **kwargs):
|
||||||
|
"""Return 404"""
|
||||||
|
return self.not_found(request)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""Return 404"""
|
||||||
|
return self.not_found(request)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""Return 404"""
|
||||||
|
return self.not_found(request)
|
||||||
|
|
||||||
|
def patch(self, request, *args, **kwargs):
|
||||||
|
"""Return 404"""
|
||||||
|
return self.not_found(request)
|
||||||
|
|
||||||
|
def put(self, request, *args, **kwargs):
|
||||||
|
"""Return 404"""
|
||||||
|
return self.not_found(request)
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
"""Return 404"""
|
||||||
|
return self.not_found(request)
|
||||||
|
|
||||||
|
|
||||||
class BulkDeleteMixin:
|
class BulkDeleteMixin:
|
||||||
|
|||||||
@@ -195,8 +195,8 @@ class InvenTreeConfig(AppConfig):
|
|||||||
else:
|
else:
|
||||||
new_user = user.objects.create_superuser(add_user, add_email, add_password)
|
new_user = user.objects.create_superuser(add_user, add_email, add_password)
|
||||||
logger.info(f'User {str(new_user)} was created!')
|
logger.info(f'User {str(new_user)} was created!')
|
||||||
except IntegrityError as _e:
|
except IntegrityError:
|
||||||
logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}')
|
logger.warning(f'The user "{add_user}" could not be created')
|
||||||
|
|
||||||
# do not try again
|
# do not try again
|
||||||
settings.USER_ADDED = True
|
settings.USER_ADDED = True
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ def convert_physical_value(value: str, unit: str = None):
|
|||||||
# At this point we *should* have a valid pint value
|
# At this point we *should* have a valid pint value
|
||||||
# To double check, look at the maginitude
|
# To double check, look at the maginitude
|
||||||
float(val.magnitude)
|
float(val.magnitude)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError, AttributeError):
|
||||||
error = _('Provided value is not a valid number')
|
error = _('Provided value is not a valid number')
|
||||||
except (pint.errors.UndefinedUnitError, pint.errors.DefinitionSyntaxError):
|
except (pint.errors.UndefinedUnitError, pint.errors.DefinitionSyntaxError):
|
||||||
error = _('Provided value has an invalid unit')
|
error = _('Provided value has an invalid unit')
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ def is_email_configured():
|
|||||||
NOTE: This does not check if the configuration is valid!
|
NOTE: This does not check if the configuration is valid!
|
||||||
"""
|
"""
|
||||||
configured = True
|
configured = True
|
||||||
|
testing = settings.TESTING
|
||||||
|
|
||||||
if InvenTree.ready.isInTestMode():
|
if InvenTree.ready.isInTestMode():
|
||||||
return False
|
return False
|
||||||
@@ -28,17 +29,24 @@ def is_email_configured():
|
|||||||
configured = False
|
configured = False
|
||||||
|
|
||||||
# Display warning unless in test mode
|
# Display warning unless in test mode
|
||||||
if not settings.TESTING: # pragma: no cover
|
if not testing: # pragma: no cover
|
||||||
logger.debug("EMAIL_HOST is not configured")
|
logger.debug("EMAIL_HOST is not configured")
|
||||||
|
|
||||||
# Display warning unless in test mode
|
# Display warning unless in test mode
|
||||||
if not settings.EMAIL_HOST_USER and not settings.TESTING: # pragma: no cover
|
if not settings.EMAIL_HOST_USER and not testing: # pragma: no cover
|
||||||
logger.debug("EMAIL_HOST_USER is not configured")
|
logger.debug("EMAIL_HOST_USER is not configured")
|
||||||
|
|
||||||
# Display warning unless in test mode
|
# Display warning unless in test mode
|
||||||
if not settings.EMAIL_HOST_PASSWORD and not settings.TESTING: # pragma: no cover
|
if not settings.EMAIL_HOST_PASSWORD and testing: # pragma: no cover
|
||||||
logger.debug("EMAIL_HOST_PASSWORD is not configured")
|
logger.debug("EMAIL_HOST_PASSWORD is not configured")
|
||||||
|
|
||||||
|
# Email sender must be configured
|
||||||
|
if not settings.DEFAULT_FROM_EMAIL:
|
||||||
|
configured = False
|
||||||
|
|
||||||
|
if not testing: # pragma: no cover
|
||||||
|
logger.warning("DEFAULT_FROM_EMAIL is not configured")
|
||||||
|
|
||||||
return configured
|
return configured
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -292,6 +292,15 @@ class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, Default
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_email_confirmation_url(self, request, emailconfirmation):
|
||||||
|
"""Construct the email confirmation url"""
|
||||||
|
|
||||||
|
from InvenTree.helpers_model import construct_absolute_url
|
||||||
|
|
||||||
|
url = super().get_email_confirmation_url(request, emailconfirmation)
|
||||||
|
url = construct_absolute_url(url)
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocialAccountAdapter):
|
class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocialAccountAdapter):
|
||||||
"""Override of adapter to use dynamic settings."""
|
"""Override of adapter to use dynamic settings."""
|
||||||
|
|||||||
@@ -50,9 +50,7 @@ def construct_absolute_url(*arg, **kwargs):
|
|||||||
# Otherwise, try to use the InvenTree setting
|
# Otherwise, try to use the InvenTree setting
|
||||||
try:
|
try:
|
||||||
site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
|
site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
|
||||||
except ProgrammingError:
|
except (ProgrammingError, OperationalError):
|
||||||
pass
|
|
||||||
except OperationalError:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not site_url:
|
if not site_url:
|
||||||
|
|||||||
@@ -601,6 +601,8 @@ DATABASES = {
|
|||||||
REMOTE_LOGIN = get_boolean_setting('INVENTREE_REMOTE_LOGIN', 'remote_login_enabled', False)
|
REMOTE_LOGIN = get_boolean_setting('INVENTREE_REMOTE_LOGIN', 'remote_login_enabled', False)
|
||||||
REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', 'remote_login_header', 'REMOTE_USER')
|
REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', 'remote_login_header', 'REMOTE_USER')
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = "/index/"
|
||||||
|
|
||||||
# sentry.io integration for error reporting
|
# sentry.io integration for error reporting
|
||||||
SENTRY_ENABLED = get_boolean_setting('INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False)
|
SENTRY_ENABLED = get_boolean_setting('INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False)
|
||||||
|
|
||||||
@@ -757,14 +759,14 @@ LANGUAGES = [
|
|||||||
('no', _('Norwegian')),
|
('no', _('Norwegian')),
|
||||||
('pl', _('Polish')),
|
('pl', _('Polish')),
|
||||||
('pt', _('Portuguese')),
|
('pt', _('Portuguese')),
|
||||||
('pt-BR', _('Portuguese (Brazilian)')),
|
('pt-br', _('Portuguese (Brazilian)')),
|
||||||
('ru', _('Russian')),
|
('ru', _('Russian')),
|
||||||
('sl', _('Slovenian')),
|
('sl', _('Slovenian')),
|
||||||
('sv', _('Swedish')),
|
('sv', _('Swedish')),
|
||||||
('th', _('Thai')),
|
('th', _('Thai')),
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('vi', _('Vietnamese')),
|
('vi', _('Vietnamese')),
|
||||||
('zh-hans', _('Chinese')),
|
('zh-hans', _('Chinese (Simplified)')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Testing interface translations
|
# Testing interface translations
|
||||||
@@ -822,6 +824,10 @@ EMAIL_USE_SSL = get_boolean_setting('INVENTREE_EMAIL_SSL', 'email.ssl', False)
|
|||||||
|
|
||||||
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
|
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
|
||||||
|
|
||||||
|
# If "from" email not specified, default to the username
|
||||||
|
if not DEFAULT_FROM_EMAIL:
|
||||||
|
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '')
|
||||||
|
|
||||||
EMAIL_USE_LOCALTIME = False
|
EMAIL_USE_LOCALTIME = False
|
||||||
EMAIL_TIMEOUT = 60
|
EMAIL_TIMEOUT = 60
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,23 @@ class ConversionTest(TestCase):
|
|||||||
q = InvenTree.conversion.convert_physical_value(val).to_base_units()
|
q = InvenTree.conversion.convert_physical_value(val).to_base_units()
|
||||||
self.assertEqual(q.magnitude, expected)
|
self.assertEqual(q.magnitude, expected)
|
||||||
|
|
||||||
|
def test_invalid_values(self):
|
||||||
|
"""Test conversion of invalid inputs"""
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
'-',
|
||||||
|
';;',
|
||||||
|
'-x',
|
||||||
|
'?',
|
||||||
|
'--',
|
||||||
|
'+',
|
||||||
|
'++',
|
||||||
|
]
|
||||||
|
|
||||||
|
for val in inputs:
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
InvenTree.conversion.convert_physical_value(val)
|
||||||
|
|
||||||
|
|
||||||
class ValidatorTest(TestCase):
|
class ValidatorTest(TestCase):
|
||||||
"""Simple tests for custom field validators."""
|
"""Simple tests for custom field validators."""
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ from django.contrib import admin
|
|||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
from django.views.generic.base import RedirectView
|
from django.views.generic.base import RedirectView
|
||||||
|
|
||||||
from dj_rest_auth.registration.views import (SocialAccountDisconnectView,
|
from dj_rest_auth.registration.views import (ConfirmEmailView,
|
||||||
|
SocialAccountDisconnectView,
|
||||||
SocialAccountListView)
|
SocialAccountListView)
|
||||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
|
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
|
||||||
|
|
||||||
@@ -74,13 +75,16 @@ apipatterns = [
|
|||||||
# InvenTree information endpoint
|
# InvenTree information endpoint
|
||||||
path('', InfoView.as_view(), name='api-inventree-info'),
|
path('', InfoView.as_view(), name='api-inventree-info'),
|
||||||
|
|
||||||
# Third party API endpoints
|
# Auth API endpoints
|
||||||
path('auth/', include('dj_rest_auth.urls')),
|
path('auth/', include([
|
||||||
path('auth/registration/', include('dj_rest_auth.registration.urls')),
|
re_path(r'^registration/account-confirm-email/(?P<key>[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'),
|
||||||
path('auth/providers/', SocialProvierListView.as_view(), name='social_providers'),
|
path('registration/', include('dj_rest_auth.registration.urls')),
|
||||||
path('auth/social/', include(social_auth_urlpatterns)),
|
path('providers/', SocialProvierListView.as_view(), name='social_providers'),
|
||||||
path('auth/social/', SocialAccountListView.as_view(), name='social_account_list'),
|
path('social/', include(social_auth_urlpatterns)),
|
||||||
path('auth/social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
|
path('social/', SocialAccountListView.as_view(), name='social_account_list'),
|
||||||
|
path('social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
|
||||||
|
path('', include('dj_rest_auth.urls')),
|
||||||
|
])),
|
||||||
|
|
||||||
# Unknown endpoint
|
# Unknown endpoint
|
||||||
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
|
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from dulwich.repo import NotGitRepository, Repo
|
|||||||
from .api_version import INVENTREE_API_VERSION
|
from .api_version import INVENTREE_API_VERSION
|
||||||
|
|
||||||
# InvenTree software version
|
# InvenTree software version
|
||||||
INVENTREE_SW_VERSION = "0.12.0"
|
INVENTREE_SW_VERSION = "0.12.4"
|
||||||
|
|
||||||
# Discover git
|
# Discover git
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -640,8 +640,12 @@ class AppearanceSelectView(RedirectView):
|
|||||||
user_theme = common_models.ColorTheme()
|
user_theme = common_models.ColorTheme()
|
||||||
user_theme.user = request.user
|
user_theme.user = request.user
|
||||||
|
|
||||||
user_theme.name = theme
|
if theme:
|
||||||
user_theme.save()
|
try:
|
||||||
|
user_theme.name = theme
|
||||||
|
user_theme.save()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return redirect(reverse_lazy('settings'))
|
return redirect(reverse_lazy('settings'))
|
||||||
|
|
||||||
|
|||||||
@@ -361,6 +361,11 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
|
|
||||||
return self.build_lines.filter(bom_item__sub_part__trackable=False)
|
return self.build_lines.filter(bom_item__sub_part__trackable=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def are_untracked_parts_allocated(self):
|
||||||
|
"""Returns True if all untracked parts are allocated for this BuildOrder."""
|
||||||
|
return self.is_fully_allocated(tracked=False)
|
||||||
|
|
||||||
def has_untracked_line_items(self):
|
def has_untracked_line_items(self):
|
||||||
"""Returns True if this BuildOrder has non trackable BomItems."""
|
"""Returns True if this BuildOrder has non trackable BomItems."""
|
||||||
return self.has_untracked_line_items.count() > 0
|
return self.has_untracked_line_items.count() > 0
|
||||||
@@ -714,14 +719,22 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
if items.exists() and items.count() == 1:
|
if items.exists() and items.count() == 1:
|
||||||
stock_item = items[0]
|
stock_item = items[0]
|
||||||
|
|
||||||
# Allocate the stock item
|
# Find the 'BuildLine' object which points to this BomItem
|
||||||
BuildItem.objects.create(
|
try:
|
||||||
build=self,
|
build_line = BuildLine.objects.get(
|
||||||
bom_item=bom_item,
|
build=self,
|
||||||
stock_item=stock_item,
|
bom_item=bom_item
|
||||||
quantity=1,
|
)
|
||||||
install_into=output,
|
|
||||||
)
|
# Allocate the stock items against the BuildLine
|
||||||
|
BuildItem.objects.create(
|
||||||
|
build_line=build_line,
|
||||||
|
stock_item=stock_item,
|
||||||
|
quantity=1,
|
||||||
|
install_into=output,
|
||||||
|
)
|
||||||
|
except BuildLine.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
"""Create a single build output of the given quantity."""
|
"""Create a single build output of the given quantity."""
|
||||||
|
|||||||
@@ -630,7 +630,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'overallocated': build.is_overallocated(),
|
'overallocated': build.is_overallocated(),
|
||||||
'allocated': build.is_fully_allocated(),
|
'allocated': build.are_untracked_parts_allocated,
|
||||||
'remaining': build.remaining,
|
'remaining': build.remaining,
|
||||||
'incomplete': build.incomplete_count,
|
'incomplete': build.incomplete_count,
|
||||||
}
|
}
|
||||||
@@ -663,7 +663,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
|||||||
"""Check if the 'accept_unallocated' field is required"""
|
"""Check if the 'accept_unallocated' field is required"""
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
if not build.is_fully_allocated() and not value:
|
if not build.are_untracked_parts_allocated and not value:
|
||||||
raise ValidationError(_('Required stock has not been fully allocated'))
|
raise ValidationError(_('Required stock has not been fully allocated'))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
kwargs: Keyword arguments to pass to the function
|
kwargs: Keyword arguments to pass to the function
|
||||||
"""
|
"""
|
||||||
# Get action
|
# Get action
|
||||||
setting = self.get_setting_definition(self.key, *args, **kwargs)
|
setting = self.get_setting_definition(self.key, *args, **{**self.get_filters_for_instance(), **kwargs})
|
||||||
settings_fnc = setting.get(reference, None)
|
settings_fnc = setting.get(reference, None)
|
||||||
|
|
||||||
# Execute if callable
|
# Execute if callable
|
||||||
|
|||||||
@@ -13,6 +13,25 @@ from InvenTree.serializers import (InvenTreeImageSerializerField,
|
|||||||
InvenTreeModelSerializer)
|
InvenTreeModelSerializer)
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsValueField(serializers.Field):
|
||||||
|
"""Custom serializer field for a settings value."""
|
||||||
|
|
||||||
|
def get_attribute(self, instance):
|
||||||
|
"""Return the object instance, not the attribute value."""
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
"""Return the value of the setting:
|
||||||
|
|
||||||
|
- Protected settings are returned as '***'
|
||||||
|
"""
|
||||||
|
return '***' if instance.protected else str(instance.value)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
"""Return the internal value of the setting"""
|
||||||
|
return str(data)
|
||||||
|
|
||||||
|
|
||||||
class SettingsSerializer(InvenTreeModelSerializer):
|
class SettingsSerializer(InvenTreeModelSerializer):
|
||||||
"""Base serializer for a settings object."""
|
"""Base serializer for a settings object."""
|
||||||
|
|
||||||
@@ -30,6 +49,8 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
api_url = serializers.CharField(read_only=True)
|
api_url = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
value = SettingsValueField()
|
||||||
|
|
||||||
def get_choices(self, obj):
|
def get_choices(self, obj):
|
||||||
"""Returns the choices available for a given item."""
|
"""Returns the choices available for a given item."""
|
||||||
results = []
|
results = []
|
||||||
@@ -45,16 +66,6 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_value(self, obj):
|
|
||||||
"""Make sure protected values are not returned."""
|
|
||||||
# never return protected values
|
|
||||||
if obj.protected:
|
|
||||||
result = '***'
|
|
||||||
else:
|
|
||||||
result = obj.value
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsSerializer(SettingsSerializer):
|
class GlobalSettingsSerializer(SettingsSerializer):
|
||||||
"""Serializer for the InvenTreeSetting model."""
|
"""Serializer for the InvenTreeSetting model."""
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
{% inventree_title %} | {% trans "Supplier List" %}
|
{% inventree_title %}{% if title %} | {{ title }}{% endif %}
|
||||||
{% endblock page_title %}
|
{% endblock page_title %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
{% if user.is_staff and perms.company.change_company %}
|
{% if user.is_staff and perms.company.change_company %}
|
||||||
{% url 'admin:company_supplierpart_change' part.pk as url %}
|
{% url 'admin:company_manufacturerpart_change' part.pk as url %}
|
||||||
{% include "admin_button.html" with url=url %}
|
{% include "admin_button.html" with url=url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if roles.purchase_order.change %}
|
{% if roles.purchase_order.change %}
|
||||||
|
|||||||
@@ -182,13 +182,15 @@ class LabelConfig(AppConfig):
|
|||||||
|
|
||||||
logger.info(f"Creating entry for {model} '{label['name']}'")
|
logger.info(f"Creating entry for {model} '{label['name']}'")
|
||||||
|
|
||||||
model.objects.create(
|
try:
|
||||||
name=label['name'],
|
model.objects.create(
|
||||||
description=label['description'],
|
name=label['name'],
|
||||||
label=filename,
|
description=label['description'],
|
||||||
filters='',
|
label=filename,
|
||||||
enabled=True,
|
filters='',
|
||||||
width=label['width'],
|
enabled=True,
|
||||||
height=label['height'],
|
width=label['width'],
|
||||||
)
|
height=label['height'],
|
||||||
return
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"Failed to create label '{label['name']}'")
|
||||||
|
|||||||
@@ -171,6 +171,14 @@ class PurchaseOrderLineItemResource(PriceResourceMixin, InvenTreeResource):
|
|||||||
|
|
||||||
SKU = Field(attribute='part__SKU', readonly=True)
|
SKU = Field(attribute='part__SKU', readonly=True)
|
||||||
|
|
||||||
|
def dehydrate_purchase_price(self, line):
|
||||||
|
"""Return a string value of the 'purchase_price' field, rather than the 'Money' object"""
|
||||||
|
|
||||||
|
if line.purchase_price:
|
||||||
|
return line.purchase_price.amount
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderExtraLineResource(PriceResourceMixin, InvenTreeResource):
|
class PurchaseOrderExtraLineResource(PriceResourceMixin, InvenTreeResource):
|
||||||
"""Class for managing import / export of PurchaseOrderExtraLine data."""
|
"""Class for managing import / export of PurchaseOrderExtraLine data."""
|
||||||
|
|||||||
@@ -2031,10 +2031,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
if bom_item.part in my_ancestors and bom_item.inherited:
|
if bom_item.part in my_ancestors and bom_item.inherited:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip if already exists
|
|
||||||
if BomItem.objects.filter(part=self, sub_part=bom_item.sub_part).exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip (or throw error) if BomItem is not valid
|
# Skip (or throw error) if BomItem is not valid
|
||||||
if not bom_item.sub_part.check_add_to_bom(self, raise_error=raise_error):
|
if not bom_item.sub_part.check_add_to_bom(self, raise_error=raise_error):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -38,9 +38,13 @@ def sso_check_provider(provider):
|
|||||||
from allauth.socialaccount.models import SocialApp
|
from allauth.socialaccount.models import SocialApp
|
||||||
|
|
||||||
# First, check that the provider is enabled
|
# First, check that the provider is enabled
|
||||||
apps = SocialApp.objects.filter(provider__iexact=provider.name)
|
apps = SocialApp.objects.filter(provider__iexact=provider.id)
|
||||||
|
|
||||||
if not apps.exists():
|
if not apps.exists():
|
||||||
|
logging.error(
|
||||||
|
"SSO SocialApp %s does not exist (known providers: %s)",
|
||||||
|
provider.id, [obj.provider for obj in SocialApp.objects.all()]
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Next, check that the provider is correctly configured
|
# Next, check that the provider is correctly configured
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi
|
|||||||
'description': 'Select a part object from the database',
|
'description': 'Select a part object from the database',
|
||||||
'model': 'part.part',
|
'model': 'part.part',
|
||||||
},
|
},
|
||||||
|
'PROTECTED_SETTING': {
|
||||||
|
'name': 'Protected Setting',
|
||||||
|
'description': 'A protected setting, hidden from the UI',
|
||||||
|
'default': 'ABC-123',
|
||||||
|
'protected': True,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NAVIGATION = [
|
NAVIGATION = [
|
||||||
|
|||||||
@@ -193,3 +193,76 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
|||||||
with self.assertRaises(NotFound) as exc:
|
with self.assertRaises(NotFound) as exc:
|
||||||
check_plugin(plugin_slug=None, plugin_pk='123')
|
check_plugin(plugin_slug=None, plugin_pk='123')
|
||||||
self.assertEqual(str(exc.exception.detail), "Plugin '123' not installed")
|
self.assertEqual(str(exc.exception.detail), "Plugin '123' not installed")
|
||||||
|
|
||||||
|
def test_plugin_settings(self):
|
||||||
|
"""Test plugin settings access via the API"""
|
||||||
|
|
||||||
|
# Ensure we have superuser permissions
|
||||||
|
self.user.is_superuser = True
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
# Activate the 'sample' plugin via the API
|
||||||
|
cfg = PluginConfig.objects.filter(key='sample').first()
|
||||||
|
url = reverse('api-plugin-detail-activate', kwargs={'pk': cfg.pk})
|
||||||
|
self.client.patch(url, {}, expected_code=200)
|
||||||
|
|
||||||
|
# Valid plugin settings endpoints
|
||||||
|
valid_settings = [
|
||||||
|
'SELECT_PART',
|
||||||
|
'API_KEY',
|
||||||
|
'NUMERICAL_SETTING',
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in valid_settings:
|
||||||
|
response = self.get(
|
||||||
|
reverse('api-plugin-setting-detail', kwargs={
|
||||||
|
'plugin': 'sample',
|
||||||
|
'key': key
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.assertEqual(response.data['key'], key)
|
||||||
|
|
||||||
|
# Test that an invalid setting key raises a 404 error
|
||||||
|
response = self.get(
|
||||||
|
reverse('api-plugin-setting-detail', kwargs={
|
||||||
|
'plugin': 'sample',
|
||||||
|
'key': 'INVALID_SETTING'
|
||||||
|
}),
|
||||||
|
expected_code=404
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that a protected setting returns hidden value
|
||||||
|
response = self.get(
|
||||||
|
reverse('api-plugin-setting-detail', kwargs={
|
||||||
|
'plugin': 'sample',
|
||||||
|
'key': 'PROTECTED_SETTING'
|
||||||
|
}),
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['value'], '***')
|
||||||
|
|
||||||
|
# Test that we can update a setting value
|
||||||
|
response = self.patch(
|
||||||
|
reverse('api-plugin-setting-detail', kwargs={
|
||||||
|
'plugin': 'sample',
|
||||||
|
'key': 'NUMERICAL_SETTING'
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
'value': 456
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['value'], '456')
|
||||||
|
|
||||||
|
# Retrieve the value again
|
||||||
|
response = self.get(
|
||||||
|
reverse('api-plugin-setting-detail', kwargs={
|
||||||
|
'plugin': 'sample',
|
||||||
|
'key': 'NUMERICAL_SETTING'
|
||||||
|
}),
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['value'], '456')
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import InvenTree.helpers
|
|||||||
import order.models
|
import order.models
|
||||||
import part.models
|
import part.models
|
||||||
from InvenTree.api import MetadataView
|
from InvenTree.api import MetadataView
|
||||||
|
from InvenTree.exceptions import log_error
|
||||||
from InvenTree.filters import InvenTreeSearchFilter
|
from InvenTree.filters import InvenTreeSearchFilter
|
||||||
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
|
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
|
||||||
from stock.models import StockItem, StockItemAttachment
|
from stock.models import StockItem, StockItemAttachment
|
||||||
@@ -181,78 +182,90 @@ class ReportPrintMixin:
|
|||||||
# Start with a default report name
|
# Start with a default report name
|
||||||
report_name = "report.pdf"
|
report_name = "report.pdf"
|
||||||
|
|
||||||
# Merge one or more PDF files into a single download
|
try:
|
||||||
for item in items_to_print:
|
# Merge one or more PDF files into a single download
|
||||||
report = self.get_object()
|
for item in items_to_print:
|
||||||
report.object_to_print = item
|
report = self.get_object()
|
||||||
|
report.object_to_print = item
|
||||||
|
|
||||||
report_name = report.generate_filename(request)
|
report_name = report.generate_filename(request)
|
||||||
output = report.render(request)
|
output = report.render(request)
|
||||||
|
|
||||||
# Run report callback for each generated report
|
# Run report callback for each generated report
|
||||||
self.report_callback(item, output, request)
|
self.report_callback(item, output, request)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if debug_mode:
|
if debug_mode:
|
||||||
outputs.append(report.render_as_string(request))
|
outputs.append(report.render_as_string(request))
|
||||||
else:
|
else:
|
||||||
outputs.append(output)
|
outputs.append(output)
|
||||||
except TemplateDoesNotExist as e:
|
except TemplateDoesNotExist as e:
|
||||||
template = str(e)
|
template = str(e)
|
||||||
if not template:
|
if not template:
|
||||||
template = report.template
|
template = report.template
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
'error': _(f"Template file '{template}' is missing or does not exist"),
|
'error': _(f"Template file '{template}' is missing or does not exist"),
|
||||||
},
|
},
|
||||||
status=400,
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not report_name.endswith('.pdf'):
|
||||||
|
report_name += '.pdf'
|
||||||
|
|
||||||
|
if debug_mode:
|
||||||
|
"""Contatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
|
||||||
|
|
||||||
|
html = "\n".join(outputs)
|
||||||
|
|
||||||
|
return HttpResponse(html)
|
||||||
|
else:
|
||||||
|
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
|
||||||
|
|
||||||
|
pages = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for output in outputs:
|
||||||
|
doc = output.get_document()
|
||||||
|
for page in doc.pages:
|
||||||
|
pages.append(page)
|
||||||
|
|
||||||
|
pdf = outputs[0].get_document().copy(pages).write_pdf()
|
||||||
|
|
||||||
|
except TemplateDoesNotExist as e:
|
||||||
|
|
||||||
|
template = str(e)
|
||||||
|
|
||||||
|
if not template:
|
||||||
|
template = report.template
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'error': _(f"Template file '{template}' is missing or does not exist"),
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user, cache=False)
|
||||||
|
|
||||||
|
return InvenTree.helpers.DownloadFile(
|
||||||
|
pdf,
|
||||||
|
report_name,
|
||||||
|
content_type='application/pdf',
|
||||||
|
inline=inline,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not report_name.endswith('.pdf'):
|
except Exception as exc:
|
||||||
report_name += '.pdf'
|
# Log the exception to the database
|
||||||
|
log_error(request.path)
|
||||||
|
|
||||||
if debug_mode:
|
# Re-throw the exception to the client as a DRF exception
|
||||||
"""Contatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
|
raise ValidationError({
|
||||||
|
'error': 'Report printing failed',
|
||||||
html = "\n".join(outputs)
|
'detail': str(exc),
|
||||||
|
'path': request.path,
|
||||||
return HttpResponse(html)
|
})
|
||||||
else:
|
|
||||||
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
|
|
||||||
|
|
||||||
pages = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
for output in outputs:
|
|
||||||
doc = output.get_document()
|
|
||||||
for page in doc.pages:
|
|
||||||
pages.append(page)
|
|
||||||
|
|
||||||
pdf = outputs[0].get_document().copy(pages).write_pdf()
|
|
||||||
|
|
||||||
except TemplateDoesNotExist as e:
|
|
||||||
|
|
||||||
template = str(e)
|
|
||||||
|
|
||||||
if not template:
|
|
||||||
template = report.template
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
'error': _(f"Template file '{template}' is missing or does not exist"),
|
|
||||||
},
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user, cache=False)
|
|
||||||
|
|
||||||
return InvenTree.helpers.DownloadFile(
|
|
||||||
pdf,
|
|
||||||
report_name,
|
|
||||||
content_type='application/pdf',
|
|
||||||
inline=inline,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Default implementation of GET for a print endpoint.
|
"""Default implementation of GET for a print endpoint.
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ def fix_purchase_price(apps, schema_editor):
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
('company', '0047_supplierpart_pack_size'),
|
||||||
('stock', '0093_auto_20230217_2140'),
|
('stock', '0093_auto_20230217_2140'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
{{ setting.description }}
|
{{ setting.description }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if setting.is_bool %}
|
{% if setting.protected %}
|
||||||
|
<span style='color: red;'>***</span> <span class='fas fa-lock icon-red'></span>
|
||||||
|
{% elif setting.is_bool %}
|
||||||
{% include "InvenTree/settings/setting_boolean.html" %}
|
{% include "InvenTree/settings/setting_boolean.html" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div id='setting-{{ setting.pk }}'>
|
<div id='setting-{{ setting.pk }}'>
|
||||||
|
|||||||
@@ -281,10 +281,20 @@ function loadAttachmentTable(url, options) {
|
|||||||
sidePagination: 'server',
|
sidePagination: 'server',
|
||||||
onPostBody: function() {
|
onPostBody: function() {
|
||||||
|
|
||||||
|
// Add callback for 'delete' button
|
||||||
|
if (permissions.delete) {
|
||||||
|
$(table).find('.button-attachment-delete').click(function() {
|
||||||
|
let pk = $(this).attr('pk');
|
||||||
|
let attachments = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
|
deleteAttachments([attachments], url, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add callback for 'edit' button
|
// Add callback for 'edit' button
|
||||||
if (permissions.change) {
|
if (permissions.change) {
|
||||||
$(table).find('.button-attachment-edit').click(function() {
|
$(table).find('.button-attachment-edit').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
let pk = $(this).attr('pk');
|
||||||
|
|
||||||
constructForm(`${url}${pk}/`, {
|
constructForm(`${url}${pk}/`, {
|
||||||
fields: {
|
fields: {
|
||||||
|
|||||||
@@ -905,6 +905,18 @@ function loadBomTable(table, options={}) {
|
|||||||
title: '{% trans "Part" %}',
|
title: '{% trans "Part" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
|
sorter: function(_valA, _valB, rowA, rowB) {
|
||||||
|
let name_a = rowA.sub_part_detail.full_name;
|
||||||
|
let name_b = rowB.sub_part_detail.full_name;
|
||||||
|
|
||||||
|
if (name_a > name_b) {
|
||||||
|
return 1;
|
||||||
|
} else if (name_a < name_b) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
var url = `/part/${row.sub_part}/`;
|
var url = `/part/${row.sub_part}/`;
|
||||||
var html = '';
|
var html = '';
|
||||||
|
|||||||
@@ -2173,9 +2173,6 @@ function loadBuildTable(table, options) {
|
|||||||
customView: function(data) {
|
customView: function(data) {
|
||||||
return `<div id='build-order-calendar'></div>`;
|
return `<div id='build-order-calendar'></div>`;
|
||||||
},
|
},
|
||||||
onRefresh: function() {
|
|
||||||
loadBuildTable(table, options);
|
|
||||||
},
|
|
||||||
onLoadSuccess: function() {
|
onLoadSuccess: function() {
|
||||||
|
|
||||||
if (tree_enable) {
|
if (tree_enable) {
|
||||||
@@ -2255,16 +2252,16 @@ function renderBuildLineAllocationTable(element, build_line, options={}) {
|
|||||||
{
|
{
|
||||||
field: 'part',
|
field: 'part',
|
||||||
title: '{% trans "Part" %}',
|
title: '{% trans "Part" %}',
|
||||||
formatter: function(value, row) {
|
formatter: function(_value, row) {
|
||||||
let html = imageHoverIcon(row.part_detail.thumbnail);
|
let html = imageHoverIcon(row.part_detail.thumbnail);
|
||||||
html += renderLink(row.part_detail.full_name, `/part/${value}/`);
|
html += renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`);
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
title: '{% trans "Allocated Quantity" %}',
|
title: '{% trans "Allocated Quantity" %}',
|
||||||
formatter: function(value, row) {
|
formatter: function(_value, row) {
|
||||||
let text = '';
|
let text = '';
|
||||||
let url = '';
|
let url = '';
|
||||||
let serial = row.serial;
|
let serial = row.serial;
|
||||||
@@ -2294,8 +2291,8 @@ function renderBuildLineAllocationTable(element, build_line, options={}) {
|
|||||||
title: '{% trans "Location" %}',
|
title: '{% trans "Location" %}',
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
if (row.location_detail) {
|
if (row.location_detail) {
|
||||||
var text = shortenString(row.location_detail.pathstring);
|
let text = shortenString(row.location_detail.pathstring);
|
||||||
var url = `/stock/location/${row.location}/`;
|
let url = `/stock/location/${row.location_detail.pk}/`;
|
||||||
|
|
||||||
return renderLink(text, url);
|
return renderLink(text, url);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1404,6 +1404,7 @@ function createPartParameter(part_id, options={}) {
|
|||||||
function editPartParameter(param_id, options={}) {
|
function editPartParameter(param_id, options={}) {
|
||||||
options.fields = partParameterFields();
|
options.fields = partParameterFields();
|
||||||
options.title = '{% trans "Edit Parameter" %}';
|
options.title = '{% trans "Edit Parameter" %}';
|
||||||
|
options.focus = 'data';
|
||||||
|
|
||||||
options.processBeforeUpload = function(data) {
|
options.processBeforeUpload = function(data) {
|
||||||
// Convert data to string
|
// Convert data to string
|
||||||
@@ -2367,6 +2368,38 @@ function loadPartTable(table, url, options={}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
|
},
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
// Display "total" stock quantity of all rendered rows
|
||||||
|
// Requires that all parts have the same base units!
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
let units = new Set();
|
||||||
|
|
||||||
|
data.forEach(function(row) {
|
||||||
|
units.add(row.units || null);
|
||||||
|
if (row.total_in_stock != null) {
|
||||||
|
total += row.total_in_stock;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.length == 0) {
|
||||||
|
return '-';
|
||||||
|
} else if (units.size > 1) {
|
||||||
|
return '-';
|
||||||
|
} else {
|
||||||
|
let output = `${total}`;
|
||||||
|
|
||||||
|
if (units.size == 1) {
|
||||||
|
let unit = units.values().next().value;
|
||||||
|
|
||||||
|
if (unit) {
|
||||||
|
output += ` [${unit}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2442,6 +2475,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
showColumns: true,
|
showColumns: true,
|
||||||
showCustomView: grid_view,
|
showCustomView: grid_view,
|
||||||
showCustomViewButton: false,
|
showCustomViewButton: false,
|
||||||
|
showFooter: true,
|
||||||
onPostBody: function() {
|
onPostBody: function() {
|
||||||
grid_view = inventreeLoad('part-grid-view') == 1;
|
grid_view = inventreeLoad('part-grid-view') == 1;
|
||||||
if (grid_view) {
|
if (grid_view) {
|
||||||
|
|||||||
@@ -1759,9 +1759,6 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
customView: function(data) {
|
customView: function(data) {
|
||||||
return `<div id='purchase-order-calendar'></div>`;
|
return `<div id='purchase-order-calendar'></div>`;
|
||||||
},
|
},
|
||||||
onRefresh: function() {
|
|
||||||
loadPurchaseOrderTable(table, options);
|
|
||||||
},
|
|
||||||
onLoadSuccess: function() {
|
onLoadSuccess: function() {
|
||||||
|
|
||||||
if (display_mode == 'calendar') {
|
if (display_mode == 'calendar') {
|
||||||
|
|||||||
@@ -262,9 +262,6 @@ function loadReturnOrderTable(table, options={}) {
|
|||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return '{% trans "No return orders found" %}';
|
return '{% trans "No return orders found" %}';
|
||||||
},
|
},
|
||||||
onRefresh: function() {
|
|
||||||
loadReturnOrderTable(table, options);
|
|
||||||
},
|
|
||||||
onLoadSuccess: function() {
|
onLoadSuccess: function() {
|
||||||
// TODO
|
// TODO
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -735,9 +735,6 @@ function loadSalesOrderTable(table, options) {
|
|||||||
customView: function(data) {
|
customView: function(data) {
|
||||||
return `<div id='purchase-order-calendar'></div>`;
|
return `<div id='purchase-order-calendar'></div>`;
|
||||||
},
|
},
|
||||||
onRefresh: function() {
|
|
||||||
loadSalesOrderTable(table, options);
|
|
||||||
},
|
|
||||||
onLoadSuccess: function() {
|
onLoadSuccess: function() {
|
||||||
|
|
||||||
if (display_mode == 'calendar') {
|
if (display_mode == 'calendar') {
|
||||||
|
|||||||
@@ -2068,13 +2068,36 @@ function loadStockTable(table, options) {
|
|||||||
// Display "total" stock quantity of all rendered rows
|
// Display "total" stock quantity of all rendered rows
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
||||||
|
// Keep track of the whether all units are the same
|
||||||
|
// If different units are found, we cannot aggregate the quantities
|
||||||
|
let units = new Set();
|
||||||
|
|
||||||
data.forEach(function(row) {
|
data.forEach(function(row) {
|
||||||
|
|
||||||
|
units.add(row.part_detail.units || null);
|
||||||
|
|
||||||
if (row.quantity != null) {
|
if (row.quantity != null) {
|
||||||
total += row.quantity;
|
total += row.quantity;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return total;
|
if (data.length == 0) {
|
||||||
|
return '-';
|
||||||
|
} else if (units.size > 1) {
|
||||||
|
return '-';
|
||||||
|
} else {
|
||||||
|
let output = `${total}`;
|
||||||
|
|
||||||
|
if (units.size == 1) {
|
||||||
|
let unit = units.values().next().value;
|
||||||
|
|
||||||
|
if (unit) {
|
||||||
|
output += ` [${unit}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2352,6 +2375,10 @@ function loadStockTable(table, options) {
|
|||||||
let row = table.bootstrapTable('getRowByUniqueId', stock_item);
|
let row = table.bootstrapTable('getRowByUniqueId', stock_item);
|
||||||
row.installed_items_received = true;
|
row.installed_items_received = true;
|
||||||
|
|
||||||
|
for (let ii = 0; ii < response.length; ii++) {
|
||||||
|
response[ii].belongs_to_item = stock_item;
|
||||||
|
}
|
||||||
|
|
||||||
table.bootstrapTable('updateByUniqueId', stock_item, row, true);
|
table.bootstrapTable('updateByUniqueId', stock_item, row, true);
|
||||||
table.bootstrapTable('append', response);
|
table.bootstrapTable('append', response);
|
||||||
|
|
||||||
@@ -2367,6 +2394,7 @@ function loadStockTable(table, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let parent_id = 'top-level';
|
let parent_id = 'top-level';
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
table.inventreeTable({
|
table.inventreeTable({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@@ -2382,13 +2410,25 @@ function loadStockTable(table, options) {
|
|||||||
showFooter: true,
|
showFooter: true,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
treeEnable: show_installed_items,
|
treeEnable: show_installed_items,
|
||||||
rootParentId: parent_id,
|
rootParentId: show_installed_items ? parent_id : null,
|
||||||
parentIdField: 'belongs_to',
|
parentIdField: show_installed_items ? 'belongs_to_item' : null,
|
||||||
uniqueId: 'pk',
|
uniqueId: 'pk',
|
||||||
idField: 'pk',
|
idField: 'pk',
|
||||||
treeShowField: 'part',
|
treeShowField: show_installed_items ? 'part' : null,
|
||||||
onPostBody: function() {
|
onLoadSuccess: function(data) {
|
||||||
|
let records = data.results || data;
|
||||||
|
|
||||||
|
// Set the 'parent' ID for each root item
|
||||||
|
if (!loaded && show_installed_items) {
|
||||||
|
for (let i = 0; i < records.length; i++) {
|
||||||
|
records[i].belongs_to_item = parent_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
|
$(table).bootstrapTable('load', records);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPostBody: function() {
|
||||||
if (show_installed_items) {
|
if (show_installed_items) {
|
||||||
table.treegrid({
|
table.treegrid({
|
||||||
treeColumn: 1,
|
treeColumn: 1,
|
||||||
@@ -2618,7 +2658,7 @@ function loadStockLocationTable(table, options) {
|
|||||||
} else {
|
} else {
|
||||||
html += `
|
html += `
|
||||||
<a href='#' pk='${row.pk}' class='load-sub-location'>
|
<a href='#' pk='${row.pk}' class='load-sub-location'>
|
||||||
<span class='fas fa-sync-alt' title='{% trans "Load Subloactions" %}'></span>
|
<span class='fas fa-sync-alt' title='{% trans "Load Sublocations" %}'></span>
|
||||||
</a> `;
|
</a> `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2809,7 +2849,7 @@ function loadStockTrackingTable(table, options) {
|
|||||||
if (details.salesorder_detail) {
|
if (details.salesorder_detail) {
|
||||||
html += renderLink(
|
html += renderLink(
|
||||||
details.salesorder_detail.reference,
|
details.salesorder_detail.reference,
|
||||||
`/order/sales-order/${details.salesorder}`
|
`/order/sales-order/${details.salesorder}/`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
html += `<em>{% trans "Sales Order no longer exists" %}</em>`;
|
html += `<em>{% trans "Sales Order no longer exists" %}</em>`;
|
||||||
|
|||||||
2
Procfile
2
Procfile
@@ -1,3 +1,3 @@
|
|||||||
web: env/bin/gunicorn --chdir $APP_HOME/InvenTree -c InvenTree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$PORT
|
web: env/bin/gunicorn --chdir $APP_HOME/InvenTree -c InvenTree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$PORT
|
||||||
worker: env/bin/python InvenTree/manage.py qcluster
|
worker: env/bin/python InvenTree/manage.py qcluster
|
||||||
cli: . env/bin/activate && exec env/bin/python -m invoke
|
cli: echo "" && . env/bin/activate && exec env/bin/python -m invoke
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ INVENTREE_DB_PORT=5432
|
|||||||
#INVENTREE_CACHE_PORT=6379
|
#INVENTREE_CACHE_PORT=6379
|
||||||
|
|
||||||
# Options for gunicorn server
|
# Options for gunicorn server
|
||||||
INVENTREE_GUNICORN_TIMEOUT=30
|
INVENTREE_GUNICORN_TIMEOUT=90
|
||||||
|
|
||||||
# Enable custom plugins?
|
# Enable custom plugins?
|
||||||
INVENTREE_PLUGINS_ENABLED=False
|
INVENTREE_PLUGINS_ENABLED=False
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Basic package requirements
|
# Basic package requirements
|
||||||
invoke>=1.4.0 # Invoke build tool
|
invoke>=1.4.0 # Invoke build tool
|
||||||
pyyaml>=6.0
|
pyyaml>=6.0.1
|
||||||
setuptools==65.6.3
|
setuptools==65.6.3
|
||||||
wheel>=0.37.0
|
wheel>=0.37.0
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ In addition to providing the ability for end-users to provide their own reportin
|
|||||||
InvenTree report templates utilize the powerful [WeasyPrint](https://weasyprint.org/) PDF generation engine.
|
InvenTree report templates utilize the powerful [WeasyPrint](https://weasyprint.org/) PDF generation engine.
|
||||||
|
|
||||||
!!! info "WeasyPrint"
|
!!! info "WeasyPrint"
|
||||||
WeasyPrint is an extremely powerful and flexible reporting library. Refer to the [WeasyPrint docs](https://weasyprint.readthedocs.io/en/stable/) for further information.
|
WeasyPrint is an extremely powerful and flexible reporting library. Refer to the [WeasyPrint docs](https://doc.courtbouillon.org/weasyprint/stable/) for further information.
|
||||||
|
|
||||||
### Stylesheets
|
### Stylesheets
|
||||||
|
|
||||||
|
|||||||
@@ -155,9 +155,16 @@ The following email settings are available:
|
|||||||
| INVENTREE_EMAIL_PASSWORD | email.password | Email account password | *Not specified* |
|
| INVENTREE_EMAIL_PASSWORD | email.password | Email account password | *Not specified* |
|
||||||
| INVENTREE_EMAIL_TLS | email.tls | Enable TLS support | False |
|
| INVENTREE_EMAIL_TLS | email.tls | Enable TLS support | False |
|
||||||
| INVENTREE_EMAIL_SSL | email.ssl | Enable SSL support | False |
|
| INVENTREE_EMAIL_SSL | email.ssl | Enable SSL support | False |
|
||||||
| INVENTREE_EMAIL_SENDER | email.sender | Name of sender | *Not specified* |
|
| INVENTREE_EMAIL_SENDER | email.sender | Sending email address | *Not specified* |
|
||||||
| INVENTREE_EMAIL_PREFIX | email.prefix | Prefix for subject text | [InvenTree] |
|
| INVENTREE_EMAIL_PREFIX | email.prefix | Prefix for subject text | [InvenTree] |
|
||||||
|
|
||||||
|
### Sender Email
|
||||||
|
|
||||||
|
The "sender" email address is the address from which InvenTree emails are sent (by default) and must be specified for outgoing emails to function:
|
||||||
|
|
||||||
|
!!! info "Fallback"
|
||||||
|
If `INVENTREE_EMAIL_SENDER` is not provided, the system will fall back to `INVENTREE_EMAIL_USERNAME` (if the username is a valid email address)
|
||||||
|
|
||||||
## Supported Currencies
|
## Supported Currencies
|
||||||
|
|
||||||
The currencies supported by InvenTree must be specified in the [configuration file](#configuration-file).
|
The currencies supported by InvenTree must be specified in the [configuration file](#configuration-file).
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ sudo apt-get install \
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! warning "Weasyprint"
|
!!! warning "Weasyprint"
|
||||||
On some systems, the dependencies for the `weasyprint` package might not be installed. Consider running through the [weasyprint installation steps](https://weasyprint.readthedocs.io/en/stable/install.html) before moving forward.
|
On some systems, the dependencies for the `weasyprint` package might not be installed. Consider running through the [weasyprint installation steps](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation) before moving forward.
|
||||||
|
|
||||||
|
|
||||||
### Create InvenTree User
|
### Create InvenTree User
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ pytz==2023.3
|
|||||||
# via
|
# via
|
||||||
# -c requirements.txt
|
# -c requirements.txt
|
||||||
# django
|
# django
|
||||||
pyyaml==6.0
|
pyyaml==6.0.1
|
||||||
# via
|
# via
|
||||||
# -c requirements.txt
|
# -c requirements.txt
|
||||||
# pre-commit
|
# pre-commit
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pillow # Image manipulation
|
|||||||
pint==0.21 # Unit conversion # FIXED 2023-05-30 breaks tests https://github.com/matmair/InvenTree/actions/runs/5095665936/jobs/9160852560
|
pint==0.21 # Unit conversion # FIXED 2023-05-30 breaks tests https://github.com/matmair/InvenTree/actions/runs/5095665936/jobs/9160852560
|
||||||
python-barcode[images] # Barcode generator
|
python-barcode[images] # Barcode generator
|
||||||
python-dotenv # Environment variable management
|
python-dotenv # Environment variable management
|
||||||
|
pyyaml>=6.0.1 # YAML parsing
|
||||||
qrcode[pil] # QR code generator
|
qrcode[pil] # QR code generator
|
||||||
rapidfuzz==0.7.6 # Fuzzy string matching
|
rapidfuzz==0.7.6 # Fuzzy string matching
|
||||||
regex # Advanced regular expressions
|
regex # Advanced regular expressions
|
||||||
|
|||||||
@@ -239,8 +239,9 @@ pytz==2023.3
|
|||||||
# django-dbbackup
|
# django-dbbackup
|
||||||
# djangorestframework
|
# djangorestframework
|
||||||
# icalendar
|
# icalendar
|
||||||
pyyaml==6.0
|
pyyaml==6.0.1
|
||||||
# via
|
# via
|
||||||
|
# -r requirements.in
|
||||||
# drf-spectacular
|
# drf-spectacular
|
||||||
# tablib
|
# tablib
|
||||||
qrcode[pil]==7.4.2
|
qrcode[pil]==7.4.2
|
||||||
|
|||||||
Reference in New Issue
Block a user