diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index e7f78554f9..4294c943ba 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -98,7 +98,7 @@ class InvenTreeMetadata(SimpleMetadata): Override get_serializer_info so that we can add 'default' values to any fields whose Meta.model specifies a default value """ - + serializer_info = super().get_serializer_info(serializer) model_class = None @@ -108,6 +108,13 @@ class InvenTreeMetadata(SimpleMetadata): model_fields = model_meta.get_field_info(model_class) + model_default_func = getattr(model_class, 'api_defaults', None) + + if model_default_func: + model_default_values = model_class.api_defaults(self.request) + else: + model_default_values = {} + # Iterate through simple fields for name, field in model_fields.fields.items(): @@ -123,6 +130,9 @@ class InvenTreeMetadata(SimpleMetadata): serializer_info[name]['default'] = default + elif name in model_default_values: + serializer_info[name]['default'] = model_default_values[name] + # Iterate through relations for name, relation in model_fields.relations.items(): @@ -141,6 +151,9 @@ class InvenTreeMetadata(SimpleMetadata): if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'): serializer_info[name]['help_text'] = relation.model_field.help_text + if name in model_default_values: + serializer_info[name]['default'] = model_default_values[name] + except AttributeError: pass diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 632ec2e0e5..cd9ce410de 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -34,8 +34,7 @@ } .login-header { - padding-right: 30px; - margin-right: 30px; + margin-right: 5px; } .login-container input { @@ -125,18 +124,16 @@ align-content: center; } -.qr-container { - width: 100%; - align-content: center; - object-fit: fill; -} - .navbar { border-bottom: 1px solid #ccc; background-color: var(--secondary-color); box-shadow: 0px 5px 5px rgb(0 0 0 / 5%); } +.inventree-navbar-menu { + position: absolute !important; +} + .navbar-brand { float: left; } @@ -545,6 +542,7 @@ .inventree-body { width: 100%; padding: 5px; + padding-right: 0; } .inventree-pre-content { @@ -835,7 +833,7 @@ input[type="submit"] { .panel { box-shadow: 2px 2px #DDD; - margin-bottom: 20px; + margin-bottom: .75rem; background-color: #fff; border: 1px solid #ccc; } diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 0dd6a404e5..4fe22f7e0e 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -47,7 +47,7 @@ def get_next_build_number(): """ if Build.objects.count() == 0: - return + return '0001' build = Build.objects.exclude(reference=None).last() @@ -100,13 +100,28 @@ class Build(MPTTModel, ReferenceIndexingMixin): return reverse('api-build-list') def api_instance_filters(self): - + return { 'parent': { 'exclude_tree': self.pk, } } + @classmethod + def api_defaults(cls, request): + """ + Return default values for this model when issuing an API OPTIONS request + """ + + defaults = { + 'reference': get_next_build_number(), + } + + if request and request.user: + defaults['issued_by'] = request.user.pk + + return defaults + def save(self, *args, **kwargs): self.rebuild_reference_field() diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 0c45e3746a..42106a7376 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -37,7 +37,7 @@ def get_next_po_number(): """ if PurchaseOrder.objects.count() == 0: - return + return '0001' order = PurchaseOrder.objects.exclude(reference=None).last() @@ -66,7 +66,7 @@ def get_next_so_number(): """ if SalesOrder.objects.count() == 0: - return + return '0001' order = SalesOrder.objects.exclude(reference=None).last() diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 23aea29dbd..3ab616d96d 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2050,10 +2050,10 @@ class Part(MPTTModel): if self.variant_of: parts.append(self.variant_of) - siblings = self.get_siblings(include_self=False) + siblings = self.get_siblings(include_self=False) - for sib in siblings: - parts.append(sib) + for sib in siblings: + parts.append(sib) filtered_parts = Part.objects.filter(pk__in=[part.pk for part in parts]) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index f64c9b0704..0f8d81203a 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -53,6 +53,12 @@ + +{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %} +{% if owner_control.value == "True" %} + {% authorized_owners item.owner as owners %} +{% endif %} + {% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %} {% if roles.stock.change and not item.is_building %}
diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index d1baf1ba6e..89fc67865a 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -212,26 +212,37 @@ {% trans "Select language" %}
- {% get_current_language as LANGUAGE_CODE %} {% get_available_languages as LANGUAGES %} {% get_language_info_list for LANGUAGES as languages %} + {% if 'alllang' in request.GET %}{% define True as ALL_LANG %}{% endif %} {% for language in languages %} {% define language.code as lang_code %} {% define locale_stats|keyvalue:lang_code as lang_translated %} + {% if lang_translated > 10 or lang_code == 'en' or lang_code == LANGUAGE_CODE %}{% define True as use_lang %}{% else %}{% define False as use_lang %}{% endif %} + {% if ALL_LANG or use_lang %} + {% endif %} {% endfor %}
+

{% trans "Some languages are not complete" %} + {% if ALL_LANG %} + . {% trans "Show only sufficent" %} + {% else %} + and hidden. {% trans "Show them too" %} + {% endif %} +

diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html index 34d4bf25e4..34884da9d1 100644 --- a/InvenTree/templates/about.html +++ b/InvenTree/templates/about.html @@ -22,12 +22,12 @@ {% inventree_version %}{% include "clip.html" %} {% inventree_is_development as dev %} {% if dev %} - {% trans "Development Version" %} + {% trans "Development Version" %} {% else %} {% if up_to_date %} - {% trans "Up to Date" %} + {% trans "Up to Date" %} {% else %} - {% trans "Update Available" %} + {% trans "Update Available" %} {% endif %} {% endif %} diff --git a/InvenTree/templates/account/base.html b/InvenTree/templates/account/base.html index 7f2486bfcc..ea3795e87c 100644 --- a/InvenTree/templates/account/base.html +++ b/InvenTree/templates/account/base.html @@ -71,9 +71,12 @@ {% include "spacer.html" %}

{% inventree_title %}

-
-
{% block content %}{% endblock %}
+
+
+ {% block content %} + {% endblock %} +
diff --git a/InvenTree/templates/account/login.html b/InvenTree/templates/account/login.html index 73c49e46c0..fbe48224b4 100644 --- a/InvenTree/templates/account/login.html +++ b/InvenTree/templates/account/login.html @@ -32,6 +32,7 @@ for a account and sign in below:{% endblocktrans %}

{% endif %} +
diff --git a/InvenTree/templates/account/logout.html b/InvenTree/templates/account/logout.html index 7268599bb2..df37c76be4 100644 --- a/InvenTree/templates/account/logout.html +++ b/InvenTree/templates/account/logout.html @@ -14,6 +14,7 @@ {% if redirect_field_value %} {% endif %} +
{% trans "Back to Site" %} diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 856deac4be..e64f1c11d0 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -83,7 +83,7 @@
-
+
{% block alerts %}
@@ -190,6 +190,18 @@ $(document).ready(function () { {% endif %} moment.locale('{{ request.LANGUAGE_CODE }}'); + + // Account notifications + {% if messages %} + {% for message in messages %} + showMessage( + '{{ message }}', + { + style: 'info', + } + ); + {% endfor %} + {% endif %} }); diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 499fde9bec..bcbbfcc6d1 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -43,11 +43,18 @@ function buildFormFields() { } }, sales_order: { + icon: 'fa-truck', }, batch: {}, - target_date: {}, - take_from: {}, - destination: {}, + target_date: { + icon: 'fa-calendar-alt', + }, + take_from: { + icon: 'fa-sitemap', + }, + destination: { + icon: 'fa-sitemap', + }, link: { icon: 'fa-link', }, diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index a86b64d0e2..18ba08d512 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -19,7 +19,6 @@ renderStockLocation, renderSupplierPart, renderUser, - showAlertDialog, showAlertOrCache, showApiError, */ @@ -347,10 +346,12 @@ function constructForm(url, options) { constructCreateForm(OPTIONS.actions.POST, options); } else { // User does not have permission to POST to the endpoint - showAlertDialog( - '{% trans "Action Prohibited" %}', - '{% trans "Create operation not allowed" %}' - ); + showMessage('{% trans "Action Prohibited" %}', { + style: 'danger', + details: '{% trans "Create operation not allowed" %}', + icon: 'fas fa-user-times', + }); + console.log(`'POST action unavailable at ${url}`); } break; @@ -360,10 +361,12 @@ function constructForm(url, options) { constructChangeForm(OPTIONS.actions.PUT, options); } else { // User does not have permission to PUT/PATCH to the endpoint - showAlertDialog( - '{% trans "Action Prohibited" %}', - '{% trans "Update operation not allowed" %}' - ); + showMessage('{% trans "Action Prohibited" %}', { + style: 'danger', + details: '{% trans "Update operation not allowed" %}', + icon: 'fas fa-user-times', + }); + console.log(`${options.method} action unavailable at ${url}`); } break; @@ -372,10 +375,12 @@ function constructForm(url, options) { constructDeleteForm(OPTIONS.actions.DELETE, options); } else { // User does not have permission to DELETE to the endpoint - showAlertDialog( - '{% trans "Action Prohibited" %}', - '{% trans "Delete operation not allowed" %}' - ); + showMessage('{% trans "Action Prohibited" %}', { + style: 'danger', + details: '{% trans "Delete operation not allowed" %}', + icon: 'fas fa-user-times', + }); + console.log(`DELETE action unavailable at ${url}`); } break; @@ -384,10 +389,12 @@ function constructForm(url, options) { // TODO? } else { // User does not have permission to GET to the endpoint - showAlertDialog( - '{% trans "Action Prohibited" %}', - '{% trans "View operation not allowed" %}' - ); + showMessage('{% trans "Action Prohibited" %}', { + style: 'danger', + details: '{% trans "View operation not allowed" %}', + icon: 'fas fa-user-times', + }); + console.log(`GET action unavailable at ${url}`); } break; diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index c63031e3cb..cd2a2a0a56 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -12,9 +12,6 @@ - {% include "search_form.html" %} -
diff --git a/InvenTree/templates/qr_code.html b/InvenTree/templates/qr_code.html index 8964ef02be..a39847629d 100644 --- a/InvenTree/templates/qr_code.html +++ b/InvenTree/templates/qr_code.html @@ -3,7 +3,7 @@
{% if qr_data %} -
+
{% else %} diff --git a/InvenTree/users/api.py b/InvenTree/users/api.py index 240d6aabc0..222f284add 100644 --- a/InvenTree/users/api.py +++ b/InvenTree/users/api.py @@ -7,15 +7,16 @@ from django.core.exceptions import ObjectDoesNotExist from django.conf.urls import url, include -from rest_framework import generics, permissions +from django_filters.rest_framework import DjangoFilterBackend + +from rest_framework import filters, generics, permissions from rest_framework.views import APIView from rest_framework.authtoken.models import Token from rest_framework.response import Response from rest_framework import status -from .serializers import UserSerializer, OwnerSerializer - -from .models import RuleSet, Owner, check_user_role +from users.models import RuleSet, Owner, check_user_role +from users.serializers import UserSerializer, OwnerSerializer class OwnerList(generics.ListAPIView): @@ -26,6 +27,37 @@ class OwnerList(generics.ListAPIView): queryset = Owner.objects.all() serializer_class = OwnerSerializer + def filter_queryset(self, queryset): + """ + Implement text search for the "owner" model. + + Note that an "owner" can be either a group, or a user, + so we cannot do a direct text search. + + A "hack" here is to post-process the queryset and simply + remove any values which do not match. + + It is not necessarily "efficient" to do it this way, + but until we determine a better way, this is what we have... + """ + + search_term = str(self.request.query_params.get('search', '')).lower() + + queryset = super().filter_queryset(queryset) + + if not search_term: + return queryset + + results = [] + + # Extract search term f + + for result in queryset.all(): + if search_term in result.name().lower(): + results.append(result) + + return results + class OwnerDetail(generics.RetrieveAPIView): """ @@ -96,6 +128,17 @@ class UserList(generics.ListAPIView): serializer_class = UserSerializer permission_classes = (permissions.IsAuthenticated,) + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + ] + + search_fields = [ + 'first_name', + 'last_name', + 'username', + ] + class GetAuthToken(APIView): """ Return authentication token for an authenticated user. """