diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 44e7ec383f..1bdfb79ceb 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -30,6 +30,8 @@ class InfoView(AjaxView): Use to confirm that the server is running, etc. """ + permission_classes = [permissions.AllowAny] + def get(self, request, *args, **kwargs): data = { diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index 7de41eef15..f9d856f566 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -17,3 +17,43 @@ def status_codes(request): 'BuildStatus': BuildStatus, 'StockStatus': StockStatus, } + + +def user_roles(request): + """ + Return a map of the current roles assigned to the user. + + Roles are denoted by their simple names, and then the permission type. + + Permissions can be access as follows: + + - roles.part.view + - roles.build.delete + + Each value will return a boolean True / False + """ + + user = request.user + + roles = { + } + + for group in user.groups.all(): + for rule in group.rule_sets.all(): + + # Ensure the role name is in the dict + if rule.name not in roles: + roles[rule.name] = { + 'view': user.is_superuser, + 'add': user.is_superuser, + 'change': user.is_superuser, + 'delete': user.is_superuser + } + + # Roles are additive across groups + roles[rule.name]['view'] |= rule.can_view + roles[rule.name]['add'] |= rule.can_add + roles[rule.name]['change'] |= rule.can_change + roles[rule.name]['delete'] |= rule.can_delete + + return {'roles': roles} diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 9b470902b1..13b770539c 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -15,6 +15,8 @@ from django.http import StreamingHttpResponse from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ +from django.contrib.auth.models import Permission + import InvenTree.version from .settings import MEDIA_URL, STATIC_URL @@ -441,3 +443,21 @@ def validateFilterString(value): results[k] = v return results + + +def addUserPermission(user, permission): + """ + Shortcut function for adding a certain permission to a user. + """ + + perm = Permission.objects.get(codename=permission) + user.user_permissions.add(perm) + + +def addUserPermissions(user, permissions): + """ + Shortcut function for adding multiple permissions to a user. + """ + + for permission in permissions: + addUserPermission(user, permission) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 21b8a0ead1..c6f8b40069 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -210,6 +210,7 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'InvenTree.context.status_codes', + 'InvenTree.context.user_roles', ], }, }, @@ -231,6 +232,10 @@ REST_FRAMEWORK = { 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + 'rest_framework.permissions.DjangoModelPermissions', + ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' } diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index d940229ebe..198903db9a 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -22,6 +22,7 @@ from django.views.generic.base import TemplateView from part.models import Part, PartCategory from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme +from users.models import check_user_role from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm from .helpers import str2bool @@ -107,31 +108,66 @@ class TreeSerializer(views.APIView): return JsonResponse(response, safe=False) -class AjaxMixin(PermissionRequiredMixin): +class InvenTreeRoleMixin(PermissionRequiredMixin): + """ + Permission class based on user roles, not user 'permissions'. + + To specify which role is required for the mixin, + set the class attribute 'role_required' to something like the following: + + role_required = 'part.add' + role_required = [ + 'part.change', + 'build.add', + ] + """ + + # By default, no roles are required + # Roles must be specified + role_required = None + + def has_permission(self): + """ + Determine if the current user + """ + + roles_required = [] + + if type(self.role_required) is str: + roles_required.append(self.role_required) + elif type(self.role_required) in [list, tuple]: + roles_required = self.role_required + + user = self.request.user + + # Superuser can have any permissions they desire + if user.is_superuser: + return True + + for required in roles_required: + + (role, permission) = required.split('.') + + # Return False if the user does not have *any* of the required roles + if not check_user_role(user, role, permission): + return False + + # We did not fail any required checks + return True + + +class AjaxMixin(InvenTreeRoleMixin): """ AjaxMixin provides basic functionality for rendering a Django form to JSON. Handles jsonResponse rendering, and adds extra data for the modal forms to process on the client side. Any view which inherits the AjaxMixin will need - correct permissions set using the 'permission_required' attribute + correct permissions set using the 'role_required' attribute """ - # By default, allow *any* permissions - permission_required = '*' - - def has_permission(self): - """ - Override the default behaviour of has_permission from PermissionRequiredMixin. - - Basically, if permission_required attribute = '*', - no permissions are actually required! - """ - - if self.permission_required == '*': - return True - else: - return super().has_permission() + # By default, allow *any* role + role_required = None # By default, point to the modal_form template # (this can be overridden by a child class) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index c0faee6c15..d4e458c506 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters -from rest_framework import generics, permissions +from rest_framework import generics from django.conf.urls import url, include @@ -28,10 +28,6 @@ class BuildList(generics.ListCreateAPIView): queryset = Build.objects.all() serializer_class = BuildSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -99,10 +95,6 @@ class BuildDetail(generics.RetrieveUpdateAPIView): queryset = Build.objects.all() serializer_class = BuildSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - class BuildItemList(generics.ListCreateAPIView): """ API endpoint for accessing a list of BuildItem objects @@ -137,10 +129,6 @@ class BuildItemList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, ] diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 915433b055..ed3da576d5 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -35,7 +35,7 @@ src="{% static 'img/blank_image.png' %}"

{{ build.quantity }} x {{ build.part.full_name }} - {% if user.is_staff and perms.build.change_build %} + {% if user.is_staff and roles.build.change %} {% endif %}

diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 0e54d7d7fb..548ac96016 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters -from rest_framework import generics, permissions +from rest_framework import generics from django.conf.urls import url, include from django.db.models import Q @@ -40,10 +40,6 @@ class CompanyList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -82,10 +78,6 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - class SupplierPartList(generics.ListCreateAPIView): """ API endpoint for list view of SupplierPart object @@ -170,10 +162,6 @@ class SupplierPartList(generics.ListCreateAPIView): serializer_class = SupplierPartSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -202,7 +190,6 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView): queryset = SupplierPart.objects.all() serializer_class = SupplierPartSerializer - permission_classes = (permissions.IsAuthenticated,) read_only_fields = [ ] @@ -218,10 +205,6 @@ class SupplierPriceBreakList(generics.ListCreateAPIView): queryset = SupplierPriceBreak.objects.all() serializer_class = SupplierPriceBreakSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, ] diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 73ebecf979..f20107277d 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -23,7 +23,7 @@ InvenTree | {% trans "Company" %} - {{ company.name }}

{{ company.name }} - {% if user.is_staff and perms.company.change_company %} + {% if user.is_staff and roles.company.change %} {% endif %}

diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py index bf4cc6643e..643608542d 100644 --- a/InvenTree/company/test_api.py +++ b/InvenTree/company/test_api.py @@ -3,6 +3,8 @@ from rest_framework import status from django.urls import reverse from django.contrib.auth import get_user_model +from InvenTree.helpers import addUserPermissions + from .models import Company @@ -14,7 +16,16 @@ class CompanyTest(APITestCase): def setUp(self): # Create a user for auth User = get_user_model() - User.objects.create_user('testuser', 'test@testing.com', 'password') + self.user = User.objects.create_user('testuser', 'test@testing.com', 'password') + + perms = [ + 'view_company', + 'change_company', + 'add_company', + ] + + addUserPermissions(self.user, perms) + self.client.login(username='testuser', password='password') Company.objects.create(name='ACME', description='Supplier', is_customer=False, is_supplier=True) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 493d4a8b06..7632fadc4c 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-04 14:02+0000\n" +"POT-Creation-Date: 2020-10-05 13:20+0000\n" "PO-Revision-Date: 2020-05-03 11:32+0200\n" "Last-Translator: Christian Schlüter \n" "Language-Team: C \n" @@ -96,7 +96,7 @@ msgstr "Datei-Kommentar" msgid "User" msgstr "Benutzer" -#: InvenTree/models.py:106 part/templates/part/params.html:20 +#: InvenTree/models.py:106 part/templates/part/params.html:22 #: templates/js/part.html:81 msgid "Name" msgstr "Name" @@ -107,19 +107,19 @@ msgstr "Name" msgid "Description (optional)" msgstr "Firmenbeschreibung" -#: InvenTree/settings.py:342 +#: InvenTree/settings.py:343 msgid "English" msgstr "Englisch" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:344 msgid "German" msgstr "Deutsch" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:345 msgid "French" msgstr "Französisch" -#: InvenTree/settings.py:345 +#: InvenTree/settings.py:346 msgid "Polish" msgstr "Polnisch" @@ -345,7 +345,7 @@ msgstr "Bau-Anzahl" msgid "Number of parts to build" msgstr "Anzahl der zu bauenden Teile" -#: build/models.py:128 part/templates/part/part_base.html:145 +#: build/models.py:128 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "Bau-Status" @@ -364,7 +364,7 @@ msgstr "Chargennummer für diese Bau-Ausgabe" #: build/models.py:155 build/templates/build/detail.html:55 #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 -#: part/templates/part/detail.html:80 part/templates/part/part_base.html:92 +#: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 #: stock/models.py:381 stock/templates/stock/item_base.html:244 msgid "External Link" msgstr "Externer Link" @@ -376,7 +376,7 @@ msgstr "Link zu einer externen URL" #: build/models.py:160 build/templates/build/tabs.html:14 company/models.py:310 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:15 #: order/templates/order/purchase_order_detail.html:202 -#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:67 +#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:70 #: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 #: stock/models.py:1404 stock/templates/stock/tabs.html:26 #: templates/js/barcode.html:391 templates/js/bom.html:223 @@ -425,7 +425,7 @@ msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" #: build/templates/build/allocate.html:17 #: company/templates/company/detail_part.html:18 order/views.py:779 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order Parts" msgstr "Teile bestellen" @@ -441,7 +441,7 @@ msgstr "Automatisches Zuweisen" msgid "Unallocate" msgstr "Zuweisung aufheben" -#: build/templates/build/allocate.html:87 templates/stock_table.html:9 +#: build/templates/build/allocate.html:87 templates/stock_table.html:10 msgid "New Stock Item" msgstr "Neues Lagerobjekt" @@ -574,7 +574,7 @@ msgstr "Lagerobjekt wurde zugewiesen" #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 #: stock/templates/stock/item_base.html:223 templates/js/build.html:39 -#: templates/navbar.html:20 +#: templates/navbar.html:25 msgid "Build" msgstr "Bau" @@ -717,7 +717,7 @@ msgstr "Fertig" #: build/templates/build/index.html:6 build/templates/build/index.html:14 #: order/templates/order/so_builds.html:11 order/templates/order/so_tabs.html:9 -#: part/templates/part/tabs.html:30 +#: part/templates/part/tabs.html:31 users/models.py:30 msgid "Build Orders" msgstr "Bauaufträge" @@ -739,7 +739,7 @@ msgstr "Speichern" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 #: order/templates/order/order_notes.html:32 #: order/templates/order/sales_order_notes.html:37 -#: part/templates/part/notes.html:32 stock/templates/stock/item_notes.html:33 +#: part/templates/part/notes.html:33 stock/templates/stock/item_notes.html:33 msgid "Edit notes" msgstr "Bermerkungen bearbeiten" @@ -1085,13 +1085,13 @@ msgid "New Supplier Part" msgstr "Neues Zulieferer-Teil" #: company/templates/company/detail_part.html:15 -#: part/templates/part/category.html:109 part/templates/part/supplier.html:15 -#: templates/stock_table.html:11 +#: part/templates/part/category.html:117 part/templates/part/supplier.html:15 +#: templates/stock_table.html:14 msgid "Options" msgstr "Optionen" #: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 #, fuzzy #| msgid "Order part" msgid "Order parts" @@ -1108,7 +1108,7 @@ msgid "Delete Parts" msgstr "Teile löschen" #: company/templates/company/detail_part.html:43 -#: part/templates/part/category.html:107 templates/js/stock.html:791 +#: part/templates/part/category.html:114 templates/js/stock.html:791 msgid "New Part" msgstr "Neues Teil" @@ -1140,7 +1140,7 @@ msgstr "Zuliefererbestand" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:106 part/templates/part/category.html:113 +#: part/templates/part/category.html:112 part/templates/part/category.html:123 #: part/templates/part/stock.html:51 templates/stock_table.html:6 msgid "Export" msgstr "Exportieren" @@ -1163,8 +1163,8 @@ msgstr "" #: company/templates/company/tabs.html:17 #: order/templates/order/purchase_orders.html:7 #: order/templates/order/purchase_orders.html:12 -#: part/templates/part/orders.html:9 part/templates/part/tabs.html:45 -#: templates/navbar.html:26 +#: part/templates/part/orders.html:9 part/templates/part/tabs.html:48 +#: templates/navbar.html:33 users/models.py:31 msgid "Purchase Orders" msgstr "Bestellungen" @@ -1182,8 +1182,8 @@ msgstr "Neue Bestellung" #: company/templates/company/tabs.html:22 #: order/templates/order/sales_orders.html:7 #: order/templates/order/sales_orders.html:12 -#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:53 -#: templates/navbar.html:33 +#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:56 +#: templates/navbar.html:42 users/models.py:32 msgid "Sales Orders" msgstr "Bestellungen" @@ -1204,7 +1204,7 @@ msgid "Supplier Part" msgstr "Zulieferer-Teil" #: company/templates/company/supplier_part_base.html:23 -#: part/templates/part/orders.html:14 +#: part/templates/part/orders.html:14 part/templates/part/part_base.html:66 msgid "Order part" msgstr "Teil bestellen" @@ -1251,7 +1251,7 @@ msgid "Pricing Information" msgstr "Preisinformationen ansehen" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2149 +#: part/templates/part/sale_prices.html:13 part/views.py:2226 msgid "Add Price Break" msgstr "Preisstaffel hinzufügen" @@ -1293,7 +1293,7 @@ msgstr "Bepreisung" #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 #: templates/js/part.html:124 templates/js/part.html:372 -#: templates/js/stock.html:452 templates/navbar.html:19 +#: templates/js/stock.html:452 templates/navbar.html:22 users/models.py:29 msgid "Stock" msgstr "Lagerbestand" @@ -1303,22 +1303,22 @@ msgstr "Bestellungen" #: company/templates/company/tabs.html:9 #: order/templates/order/receive_parts.html:14 part/models.py:294 -#: part/templates/part/cat_link.html:7 part/templates/part/category.html:88 -#: part/templates/part/category_tabs.html:6 templates/navbar.html:18 -#: templates/stats.html:8 templates/stats.html:17 +#: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 +#: part/templates/part/category_tabs.html:6 templates/navbar.html:19 +#: templates/stats.html:8 templates/stats.html:17 users/models.py:28 msgid "Parts" msgstr "Teile" -#: company/views.py:50 part/templates/part/tabs.html:39 -#: templates/navbar.html:24 +#: company/views.py:50 part/templates/part/tabs.html:42 +#: templates/navbar.html:31 msgid "Suppliers" msgstr "Zulieferer" -#: company/views.py:57 templates/navbar.html:25 +#: company/views.py:57 templates/navbar.html:32 msgid "Manufacturers" msgstr "Hersteller" -#: company/views.py:64 templates/navbar.html:32 +#: company/views.py:64 templates/navbar.html:41 msgid "Customers" msgstr "Kunden" @@ -1382,17 +1382,17 @@ msgstr "Neues Zuliefererteil anlegen" msgid "Delete Supplier Part" msgstr "Zuliefererteil entfernen" -#: company/views.py:404 part/views.py:2153 +#: company/views.py:404 part/views.py:2232 #, fuzzy #| msgid "Add Price Break" msgid "Added new price break" msgstr "Preisstaffel hinzufügen" -#: company/views.py:441 part/views.py:2198 +#: company/views.py:441 part/views.py:2277 msgid "Edit Price Break" msgstr "Preisstaffel bearbeiten" -#: company/views.py:456 part/views.py:2212 +#: company/views.py:456 part/views.py:2293 msgid "Delete Price Break" msgstr "Preisstaffel löschen" @@ -1497,7 +1497,7 @@ msgstr "" msgid "Date order was completed" msgstr "Bestellung als vollständig markieren" -#: order/models.py:185 order/models.py:259 part/views.py:1304 +#: order/models.py:185 order/models.py:259 part/views.py:1343 #: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" @@ -1667,7 +1667,7 @@ msgid "Purchase Order Attachments" msgstr "Bestellanhänge" #: order/templates/order/po_tabs.html:8 order/templates/order/so_tabs.html:16 -#: part/templates/part/tabs.html:64 stock/templates/stock/tabs.html:32 +#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 msgid "Attachments" msgstr "Anhänge" @@ -1683,7 +1683,7 @@ msgstr "Bestellpositionen" #: order/templates/order/purchase_order_detail.html:38 #: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:161 part/templates/part/category.html:202 +#: part/templates/part/category.html:171 part/templates/part/category.html:213 #: templates/js/stock.html:803 msgid "New Location" msgstr "Neuer Standort" @@ -1725,7 +1725,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:135 templates/js/part.html:388 +#: part/templates/part/part_base.html:145 templates/js/part.html:388 msgid "On Order" msgstr "bestellt" @@ -1823,7 +1823,7 @@ msgstr "Bestellungspositionen" msgid "Add Purchase Order Attachment" msgstr "Bestellanhang hinzufügen" -#: order/views.py:102 order/views.py:149 part/views.py:86 stock/views.py:167 +#: order/views.py:102 order/views.py:149 part/views.py:90 stock/views.py:167 msgid "Added attachment" msgstr "Anhang hinzugefügt" @@ -1963,12 +1963,12 @@ msgstr "Zuordnung bearbeiten" msgid "Remove allocation" msgstr "Zuordnung entfernen" -#: part/bom.py:138 part/templates/part/category.html:55 +#: part/bom.py:138 part/templates/part/category.html:61 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "Standard-Lagerort" -#: part/bom.py:139 part/templates/part/part_base.html:108 +#: part/bom.py:139 part/templates/part/part_base.html:118 msgid "Available Stock" msgstr "Verfügbarer Lagerbestand" @@ -2100,7 +2100,7 @@ msgid "Part Category" msgstr "Teilkategorie" #: part/models.py:76 part/templates/part/category.html:18 -#: part/templates/part/category.html:83 templates/stats.html:12 +#: part/templates/part/category.html:89 templates/stats.html:12 msgid "Part Categories" msgstr "Teile-Kategorien" @@ -2339,7 +2339,7 @@ msgstr "Notizen zum Stücklisten-Objekt" msgid "BOM line checksum" msgstr "Prüfsumme der Stückliste" -#: part/models.py:1612 part/views.py:1310 part/views.py:1362 +#: part/models.py:1612 part/views.py:1349 part/views.py:1401 #: stock/models.py:231 #, fuzzy #| msgid "Overage must be an integer value or a percentage" @@ -2402,25 +2402,25 @@ msgstr "Neue Stücklistenposition" msgid "Finish Editing" msgstr "Bearbeitung beenden" -#: part/templates/part/bom.html:42 +#: part/templates/part/bom.html:43 msgid "Edit BOM" msgstr "Stückliste bearbeiten" -#: part/templates/part/bom.html:44 +#: part/templates/part/bom.html:45 msgid "Validate Bill of Materials" msgstr "Stückliste validieren" -#: part/templates/part/bom.html:46 part/views.py:1597 +#: part/templates/part/bom.html:48 part/views.py:1640 msgid "Export Bill of Materials" msgstr "Stückliste exportieren" -#: part/templates/part/bom.html:101 +#: part/templates/part/bom.html:103 #, fuzzy #| msgid "Remove selected BOM items" msgid "Delete selected BOM items?" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: part/templates/part/bom.html:102 +#: part/templates/part/bom.html:104 #, fuzzy #| msgid "Remove selected BOM items" msgid "All selected BOM items will be deleted" @@ -2510,101 +2510,113 @@ msgstr "Neues Bild hochladen" msgid "Each part must already exist in the database" msgstr "" +#: part/templates/part/build.html:8 +#, fuzzy +#| msgid "Parent Build" +msgid "Part Builds" +msgstr "Eltern-Bau" + +#: part/templates/part/build.html:14 +#, fuzzy +#| msgid "Start new Build" +msgid "Start New Build" +msgstr "Neuen Bau beginnen" + #: part/templates/part/category.html:19 msgid "All parts" msgstr "Alle Teile" -#: part/templates/part/category.html:23 part/views.py:1976 +#: part/templates/part/category.html:24 part/views.py:2043 msgid "Create new part category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:27 +#: part/templates/part/category.html:30 #, fuzzy #| msgid "Edit Part Category" msgid "Edit part category" msgstr "Teilkategorie bearbeiten" -#: part/templates/part/category.html:30 +#: part/templates/part/category.html:35 #, fuzzy #| msgid "Select part category" msgid "Delete part category" msgstr "Teilekategorie wählen" -#: part/templates/part/category.html:39 part/templates/part/category.html:78 +#: part/templates/part/category.html:45 part/templates/part/category.html:84 msgid "Category Details" msgstr "Kategorie-Details" -#: part/templates/part/category.html:44 +#: part/templates/part/category.html:50 msgid "Category Path" msgstr "Pfad zur Kategorie" -#: part/templates/part/category.html:49 +#: part/templates/part/category.html:55 msgid "Category Description" msgstr "Kategorie-Beschreibung" -#: part/templates/part/category.html:62 part/templates/part/detail.html:64 +#: part/templates/part/category.html:68 part/templates/part/detail.html:64 msgid "Keywords" msgstr "Schlüsselwörter" -#: part/templates/part/category.html:68 +#: part/templates/part/category.html:74 msgid "Subcategories" msgstr "Unter-Kategorien" -#: part/templates/part/category.html:73 +#: part/templates/part/category.html:79 msgid "Parts (Including subcategories)" msgstr "Teile (inklusive Unter-Kategorien)" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:112 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:107 part/views.py:491 +#: part/templates/part/category.html:114 part/views.py:511 msgid "Create new part" msgstr "Neues Teil anlegen" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 #, fuzzy #| msgid "Part category" msgid "Set category" msgstr "Teile-Kategorie" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 #, fuzzy #| msgid "Set Part Category" msgid "Set Category" msgstr "Teilkategorie auswählen" -#: part/templates/part/category.html:113 +#: part/templates/part/category.html:123 #, fuzzy #| msgid "Export" msgid "Export Data" msgstr "Exportieren" -#: part/templates/part/category.html:162 +#: part/templates/part/category.html:172 #, fuzzy #| msgid "Create New Location" msgid "Create new location" msgstr "Neuen Standort anlegen" -#: part/templates/part/category.html:167 part/templates/part/category.html:196 +#: part/templates/part/category.html:177 part/templates/part/category.html:207 #, fuzzy #| msgid "Category" msgid "New Category" msgstr "Kategorie" -#: part/templates/part/category.html:168 +#: part/templates/part/category.html:178 #, fuzzy #| msgid "Create new part category" msgid "Create new category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:197 +#: part/templates/part/category.html:208 #, fuzzy #| msgid "Create new part category" msgid "Create new Part Category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:203 stock/views.py:1314 +#: part/templates/part/category.html:214 stock/views.py:1314 msgid "Create new Stock Location" msgstr "Neuen Lager-Standort erstellen" @@ -2618,7 +2630,7 @@ msgstr "Parameter Wert" msgid "Part Details" msgstr "Teile-Details" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:85 +#: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 #: templates/js/part.html:112 msgid "IPN" msgstr "IPN (Interne Produktnummer)" @@ -2652,7 +2664,7 @@ msgstr "Kategorie" msgid "Default Supplier" msgstr "Standard-Zulieferer" -#: part/templates/part/detail.html:102 part/templates/part/params.html:22 +#: part/templates/part/detail.html:102 part/templates/part/params.html:24 msgid "Units" msgstr "Einheiten" @@ -2785,24 +2797,25 @@ msgstr "Teil bestellen" msgid "Part Parameters" msgstr "Teilparameter" -#: part/templates/part/params.html:13 +#: part/templates/part/params.html:14 msgid "Add new parameter" msgstr "Parameter hinzufügen" -#: part/templates/part/params.html:13 templates/InvenTree/settings/part.html:12 +#: part/templates/part/params.html:14 templates/InvenTree/settings/part.html:12 msgid "New Parameter" msgstr "Neuer Parameter" -#: part/templates/part/params.html:21 stock/models.py:1391 +#: part/templates/part/params.html:23 stock/models.py:1391 #: templates/js/stock.html:112 msgid "Value" msgstr "Wert" -#: part/templates/part/params.html:33 +#: part/templates/part/params.html:36 msgid "Edit" msgstr "Bearbeiten" -#: part/templates/part/params.html:34 part/templates/part/supplier.html:17 +#: part/templates/part/params.html:39 part/templates/part/supplier.html:17 +#: users/models.py:141 msgid "Delete" msgstr "Löschen" @@ -2859,47 +2872,53 @@ msgstr "" msgid "Show pricing information" msgstr "Kosteninformationen ansehen" -#: part/templates/part/part_base.html:70 +#: part/templates/part/part_base.html:60 +#, fuzzy +#| msgid "Count stock" +msgid "Count part stock" +msgstr "Bestand zählen" + +#: part/templates/part/part_base.html:75 #, fuzzy #| msgid "Source Location" msgid "Part actions" msgstr "Quell-Standort" -#: part/templates/part/part_base.html:72 +#: part/templates/part/part_base.html:78 #, fuzzy #| msgid "Duplicate Part" msgid "Duplicate part" msgstr "Teil duplizieren" -#: part/templates/part/part_base.html:73 +#: part/templates/part/part_base.html:81 #, fuzzy #| msgid "Edit Template" msgid "Edit part" msgstr "Vorlage bearbeiten" -#: part/templates/part/part_base.html:75 +#: part/templates/part/part_base.html:84 #, fuzzy #| msgid "Delete Parts" msgid "Delete part" msgstr "Teile löschen" -#: part/templates/part/part_base.html:114 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:124 templates/js/table_filters.html:65 msgid "In Stock" msgstr "Auf Lager" -#: part/templates/part/part_base.html:121 +#: part/templates/part/part_base.html:131 msgid "Allocated to Build Orders" msgstr "Zu Bauaufträgen zugeordnet" -#: part/templates/part/part_base.html:128 +#: part/templates/part/part_base.html:138 msgid "Allocated to Sales Orders" msgstr "Zu Aufträgen zugeordnet" -#: part/templates/part/part_base.html:150 +#: part/templates/part/part_base.html:160 msgid "Can Build" msgstr "Herstellbar?" -#: part/templates/part/part_base.html:156 +#: part/templates/part/part_base.html:166 msgid "Underway" msgstr "unterwegs" @@ -2923,7 +2942,7 @@ msgstr "Aus vorhandenen Bildern auswählen" msgid "Upload new image" msgstr "Neues Bild hochladen" -#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:50 +#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:53 #, fuzzy #| msgid "Price" msgid "Sale Price" @@ -2994,11 +3013,11 @@ msgstr "Varianten" msgid "BOM" msgstr "Stückliste" -#: part/templates/part/tabs.html:34 +#: part/templates/part/tabs.html:37 msgid "Used In" msgstr "Benutzt in" -#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:282 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:282 msgid "Tests" msgstr "" @@ -3032,184 +3051,184 @@ msgstr "Neues Teil hinzufügen" msgid "New Variant" msgstr "Varianten" -#: part/views.py:76 +#: part/views.py:78 msgid "Add part attachment" msgstr "Teilanhang hinzufügen" -#: part/views.py:125 templates/attachment_table.html:30 +#: part/views.py:129 templates/attachment_table.html:30 msgid "Edit attachment" msgstr "Anhang bearbeiten" -#: part/views.py:129 +#: part/views.py:135 msgid "Part attachment updated" msgstr "Teilanhang aktualisiert" -#: part/views.py:144 +#: part/views.py:150 msgid "Delete Part Attachment" msgstr "Teilanhang löschen" -#: part/views.py:150 +#: part/views.py:158 msgid "Deleted part attachment" msgstr "Teilanhang gelöscht" -#: part/views.py:159 +#: part/views.py:167 #, fuzzy #| msgid "Create Part Parameter Template" msgid "Create Test Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:186 +#: part/views.py:196 #, fuzzy #| msgid "Edit Template" msgid "Edit Test Template" msgstr "Vorlage bearbeiten" -#: part/views.py:200 +#: part/views.py:212 #, fuzzy #| msgid "Delete Template" msgid "Delete Test Template" msgstr "Vorlage löschen" -#: part/views.py:207 +#: part/views.py:221 msgid "Set Part Category" msgstr "Teilkategorie auswählen" -#: part/views.py:255 +#: part/views.py:271 #, python-brace-format msgid "Set category for {n} parts" msgstr "Kategorie für {n} Teile setzen" -#: part/views.py:290 +#: part/views.py:306 msgid "Create Variant" msgstr "Variante anlegen" -#: part/views.py:368 +#: part/views.py:386 msgid "Duplicate Part" msgstr "Teil duplizieren" -#: part/views.py:373 +#: part/views.py:393 msgid "Copied part" msgstr "Teil kopiert" -#: part/views.py:496 +#: part/views.py:518 msgid "Created new part" msgstr "Neues Teil angelegt" -#: part/views.py:707 +#: part/views.py:733 msgid "Part QR Code" msgstr "Teil-QR-Code" -#: part/views.py:724 +#: part/views.py:752 msgid "Upload Part Image" msgstr "Teilbild hochladen" -#: part/views.py:729 part/views.py:764 +#: part/views.py:760 part/views.py:797 msgid "Updated part image" msgstr "Teilbild aktualisiert" -#: part/views.py:738 +#: part/views.py:769 msgid "Select Part Image" msgstr "Teilbild auswählen" -#: part/views.py:767 +#: part/views.py:800 msgid "Part image not found" msgstr "Teilbild nicht gefunden" -#: part/views.py:778 +#: part/views.py:811 msgid "Edit Part Properties" msgstr "Teileigenschaften bearbeiten" -#: part/views.py:800 +#: part/views.py:835 msgid "Validate BOM" msgstr "BOM validieren" -#: part/views.py:963 +#: part/views.py:1002 msgid "No BOM file provided" msgstr "Keine Stückliste angegeben" -#: part/views.py:1313 +#: part/views.py:1352 msgid "Enter a valid quantity" msgstr "Bitte eine gültige Anzahl eingeben" -#: part/views.py:1338 part/views.py:1341 +#: part/views.py:1377 part/views.py:1380 msgid "Select valid part" msgstr "Bitte ein gültiges Teil auswählen" -#: part/views.py:1347 +#: part/views.py:1386 msgid "Duplicate part selected" msgstr "Teil doppelt ausgewählt" -#: part/views.py:1385 +#: part/views.py:1424 msgid "Select a part" msgstr "Teil auswählen" -#: part/views.py:1391 +#: part/views.py:1430 #, fuzzy #| msgid "Select part to be used in BOM" msgid "Selected part creates a circular BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/views.py:1395 +#: part/views.py:1434 msgid "Specify quantity" msgstr "Anzahl angeben" -#: part/views.py:1645 +#: part/views.py:1690 msgid "Confirm Part Deletion" msgstr "Löschen des Teils bestätigen" -#: part/views.py:1652 +#: part/views.py:1699 msgid "Part was deleted" msgstr "Teil wurde gelöscht" -#: part/views.py:1661 +#: part/views.py:1708 msgid "Part Pricing" msgstr "Teilbepreisung" -#: part/views.py:1783 +#: part/views.py:1834 msgid "Create Part Parameter Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:1791 +#: part/views.py:1844 msgid "Edit Part Parameter Template" msgstr "Teilparametervorlage bearbeiten" -#: part/views.py:1798 +#: part/views.py:1853 msgid "Delete Part Parameter Template" msgstr "Teilparametervorlage löschen" -#: part/views.py:1806 +#: part/views.py:1863 msgid "Create Part Parameter" msgstr "Teilparameter anlegen" -#: part/views.py:1856 +#: part/views.py:1915 msgid "Edit Part Parameter" msgstr "Teilparameter bearbeiten" -#: part/views.py:1870 +#: part/views.py:1931 msgid "Delete Part Parameter" msgstr "Teilparameter löschen" -#: part/views.py:1927 +#: part/views.py:1990 msgid "Edit Part Category" msgstr "Teilkategorie bearbeiten" -#: part/views.py:1962 +#: part/views.py:2027 msgid "Delete Part Category" msgstr "Teilkategorie löschen" -#: part/views.py:1968 +#: part/views.py:2035 msgid "Part category was deleted" msgstr "Teilekategorie wurde gelöscht" -#: part/views.py:2027 +#: part/views.py:2098 msgid "Create BOM item" msgstr "BOM-Position anlegen" -#: part/views.py:2093 +#: part/views.py:2166 msgid "Edit BOM item" msgstr "BOM-Position beaarbeiten" -#: part/views.py:2141 +#: part/views.py:2216 msgid "Confim BOM item deletion" msgstr "Löschung von BOM-Position bestätigen" @@ -3653,15 +3672,15 @@ msgid "Stock adjustment actions" msgstr "Bestands-Anpassung bestätigen" #: stock/templates/stock/item_base.html:98 -#: stock/templates/stock/location.html:38 templates/stock_table.html:15 +#: stock/templates/stock/location.html:38 templates/stock_table.html:19 msgid "Count stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:99 templates/stock_table.html:13 +#: stock/templates/stock/item_base.html:99 templates/stock_table.html:17 msgid "Add stock" msgstr "Bestand hinzufügen" -#: stock/templates/stock/item_base.html:100 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:100 templates/stock_table.html:18 msgid "Remove stock" msgstr "Bestand entfernen" @@ -4191,6 +4210,14 @@ msgstr "Lagerbestands-Tracking-Eintrag bearbeiten" msgid "Add Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag hinzufügen" +#: templates/403.html:5 templates/403.html:11 +msgid "Permission Denied" +msgstr "" + +#: templates/403.html:14 +msgid "You do not have permission to view this page." +msgstr "" + #: templates/InvenTree/bom_invalid.html:7 msgid "BOM Waiting Validation" msgstr "" @@ -4201,6 +4228,10 @@ msgstr "" msgid "Pending Builds" msgstr "Eltern-Bau" +#: templates/InvenTree/index.html:4 +msgid "Index" +msgstr "" + #: templates/InvenTree/latest_parts.html:7 #, fuzzy #| msgid "Parent Part" @@ -4884,39 +4915,39 @@ msgstr "Favorit" msgid "Purchasable" msgstr "Käuflich" -#: templates/navbar.html:22 +#: templates/navbar.html:29 msgid "Buy" msgstr "Kaufen" -#: templates/navbar.html:30 +#: templates/navbar.html:39 msgid "Sell" msgstr "Verkaufen" -#: templates/navbar.html:40 +#: templates/navbar.html:50 msgid "Scan Barcode" msgstr "" -#: templates/navbar.html:49 +#: templates/navbar.html:59 users/models.py:27 msgid "Admin" msgstr "Admin" -#: templates/navbar.html:52 +#: templates/navbar.html:62 msgid "Settings" msgstr "Einstellungen" -#: templates/navbar.html:53 +#: templates/navbar.html:63 msgid "Logout" msgstr "Ausloggen" -#: templates/navbar.html:55 +#: templates/navbar.html:65 msgid "Login" msgstr "Einloggen" -#: templates/navbar.html:58 +#: templates/navbar.html:68 msgid "About InvenTree" msgstr "Über InvenBaum" -#: templates/navbar.html:59 +#: templates/navbar.html:69 msgid "Statistics" msgstr "Statistiken" @@ -4930,54 +4961,124 @@ msgstr "Suche" msgid "Export Stock Information" msgstr "Lagerobjekt-Standort bearbeiten" -#: templates/stock_table.html:13 +#: templates/stock_table.html:17 #, fuzzy #| msgid "Added stock to {n} items" msgid "Add to selected stock items" msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" -#: templates/stock_table.html:14 +#: templates/stock_table.html:18 #, fuzzy #| msgid "Remove selected BOM items" msgid "Remove from selected stock items" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: templates/stock_table.html:15 +#: templates/stock_table.html:19 #, fuzzy #| msgid "Delete Stock Item" msgid "Stocktake selected stock items" msgstr "Lagerobjekt löschen" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 #, fuzzy #| msgid "Delete Stock Item" msgid "Move selected stock items" msgstr "Lagerobjekt löschen" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move stock" msgstr "Bestand bewegen" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 #, fuzzy #| msgid "Remove selected BOM items" msgid "Order selected items" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order stock" msgstr "Bestand bestellen" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 #, fuzzy #| msgid "Delete line item" msgid "Delete selected items" msgstr "Position löschen" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete Stock" msgstr "Bestand löschen" +#: users/admin.py:62 +#, fuzzy +#| msgid "User" +msgid "Users" +msgstr "Benutzer" + +#: users/admin.py:63 +msgid "Select which users are assigned to this group" +msgstr "" + +#: users/admin.py:124 +#, fuzzy +#| msgid "External Link" +msgid "Personal info" +msgstr "Externer Link" + +#: users/admin.py:125 +#, fuzzy +#| msgid "Revision" +msgid "Permissions" +msgstr "Revision" + +#: users/admin.py:128 +#, fuzzy +#| msgid "Import BOM data" +msgid "Important dates" +msgstr "Stückliste importieren" + +#: users/models.py:124 +msgid "Permission set" +msgstr "" + +#: users/models.py:132 +msgid "Group" +msgstr "" + +#: users/models.py:135 +msgid "View" +msgstr "" + +#: users/models.py:135 +msgid "Permission to view items" +msgstr "" + +#: users/models.py:137 +#, fuzzy +#| msgid "Created" +msgid "Create" +msgstr "Erstellt" + +#: users/models.py:137 +msgid "Permission to add items" +msgstr "" + +#: users/models.py:139 +#, fuzzy +#| msgid "Last Updated" +msgid "Update" +msgstr "Zuletzt aktualisiert" + +#: users/models.py:139 +msgid "Permissions to edit items" +msgstr "" + +#: users/models.py:141 +#, fuzzy +#| msgid "Remove selected BOM items" +msgid "Permission to delete items" +msgstr "Ausgewählte Stücklistenpositionen entfernen" + #~ msgid "Belongs To" #~ msgstr "Gehört zu" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index 0b5425db6e..0f38022f92 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-04 14:02+0000\n" +"POT-Creation-Date: 2020-10-05 13:20+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -90,7 +90,7 @@ msgstr "" msgid "User" msgstr "" -#: InvenTree/models.py:106 part/templates/part/params.html:20 +#: InvenTree/models.py:106 part/templates/part/params.html:22 #: templates/js/part.html:81 msgid "Name" msgstr "" @@ -99,19 +99,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:342 +#: InvenTree/settings.py:343 msgid "English" msgstr "" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:344 msgid "German" msgstr "" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:345 msgid "French" msgstr "" -#: InvenTree/settings.py:345 +#: InvenTree/settings.py:346 msgid "Polish" msgstr "" @@ -325,7 +325,7 @@ msgstr "" msgid "Number of parts to build" msgstr "" -#: build/models.py:128 part/templates/part/part_base.html:145 +#: build/models.py:128 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "" @@ -344,7 +344,7 @@ msgstr "" #: build/models.py:155 build/templates/build/detail.html:55 #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 -#: part/templates/part/detail.html:80 part/templates/part/part_base.html:92 +#: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 #: stock/models.py:381 stock/templates/stock/item_base.html:244 msgid "External Link" msgstr "" @@ -356,7 +356,7 @@ msgstr "" #: build/models.py:160 build/templates/build/tabs.html:14 company/models.py:310 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:15 #: order/templates/order/purchase_order_detail.html:202 -#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:67 +#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:70 #: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 #: stock/models.py:1404 stock/templates/stock/tabs.html:26 #: templates/js/barcode.html:391 templates/js/bom.html:223 @@ -404,7 +404,7 @@ msgstr "" #: build/templates/build/allocate.html:17 #: company/templates/company/detail_part.html:18 order/views.py:779 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order Parts" msgstr "" @@ -420,7 +420,7 @@ msgstr "" msgid "Unallocate" msgstr "" -#: build/templates/build/allocate.html:87 templates/stock_table.html:9 +#: build/templates/build/allocate.html:87 templates/stock_table.html:10 msgid "New Stock Item" msgstr "" @@ -548,7 +548,7 @@ msgstr "" #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 #: stock/templates/stock/item_base.html:223 templates/js/build.html:39 -#: templates/navbar.html:20 +#: templates/navbar.html:25 msgid "Build" msgstr "" @@ -687,7 +687,7 @@ msgstr "" #: build/templates/build/index.html:6 build/templates/build/index.html:14 #: order/templates/order/so_builds.html:11 order/templates/order/so_tabs.html:9 -#: part/templates/part/tabs.html:30 +#: part/templates/part/tabs.html:31 users/models.py:30 msgid "Build Orders" msgstr "" @@ -709,7 +709,7 @@ msgstr "" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 #: order/templates/order/order_notes.html:32 #: order/templates/order/sales_order_notes.html:37 -#: part/templates/part/notes.html:32 stock/templates/stock/item_notes.html:33 +#: part/templates/part/notes.html:33 stock/templates/stock/item_notes.html:33 msgid "Edit notes" msgstr "" @@ -1044,13 +1044,13 @@ msgid "New Supplier Part" msgstr "" #: company/templates/company/detail_part.html:15 -#: part/templates/part/category.html:109 part/templates/part/supplier.html:15 -#: templates/stock_table.html:11 +#: part/templates/part/category.html:117 part/templates/part/supplier.html:15 +#: templates/stock_table.html:14 msgid "Options" msgstr "" #: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order parts" msgstr "" @@ -1063,7 +1063,7 @@ msgid "Delete Parts" msgstr "" #: company/templates/company/detail_part.html:43 -#: part/templates/part/category.html:107 templates/js/stock.html:791 +#: part/templates/part/category.html:114 templates/js/stock.html:791 msgid "New Part" msgstr "" @@ -1095,7 +1095,7 @@ msgstr "" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:106 part/templates/part/category.html:113 +#: part/templates/part/category.html:112 part/templates/part/category.html:123 #: part/templates/part/stock.html:51 templates/stock_table.html:6 msgid "Export" msgstr "" @@ -1117,8 +1117,8 @@ msgstr "" #: company/templates/company/tabs.html:17 #: order/templates/order/purchase_orders.html:7 #: order/templates/order/purchase_orders.html:12 -#: part/templates/part/orders.html:9 part/templates/part/tabs.html:45 -#: templates/navbar.html:26 +#: part/templates/part/orders.html:9 part/templates/part/tabs.html:48 +#: templates/navbar.html:33 users/models.py:31 msgid "Purchase Orders" msgstr "" @@ -1136,8 +1136,8 @@ msgstr "" #: company/templates/company/tabs.html:22 #: order/templates/order/sales_orders.html:7 #: order/templates/order/sales_orders.html:12 -#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:53 -#: templates/navbar.html:33 +#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:56 +#: templates/navbar.html:42 users/models.py:32 msgid "Sales Orders" msgstr "" @@ -1158,7 +1158,7 @@ msgid "Supplier Part" msgstr "" #: company/templates/company/supplier_part_base.html:23 -#: part/templates/part/orders.html:14 +#: part/templates/part/orders.html:14 part/templates/part/part_base.html:66 msgid "Order part" msgstr "" @@ -1205,7 +1205,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2149 +#: part/templates/part/sale_prices.html:13 part/views.py:2226 msgid "Add Price Break" msgstr "" @@ -1241,7 +1241,7 @@ msgstr "" #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 #: templates/js/part.html:124 templates/js/part.html:372 -#: templates/js/stock.html:452 templates/navbar.html:19 +#: templates/js/stock.html:452 templates/navbar.html:22 users/models.py:29 msgid "Stock" msgstr "" @@ -1251,22 +1251,22 @@ msgstr "" #: company/templates/company/tabs.html:9 #: order/templates/order/receive_parts.html:14 part/models.py:294 -#: part/templates/part/cat_link.html:7 part/templates/part/category.html:88 -#: part/templates/part/category_tabs.html:6 templates/navbar.html:18 -#: templates/stats.html:8 templates/stats.html:17 +#: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 +#: part/templates/part/category_tabs.html:6 templates/navbar.html:19 +#: templates/stats.html:8 templates/stats.html:17 users/models.py:28 msgid "Parts" msgstr "" -#: company/views.py:50 part/templates/part/tabs.html:39 -#: templates/navbar.html:24 +#: company/views.py:50 part/templates/part/tabs.html:42 +#: templates/navbar.html:31 msgid "Suppliers" msgstr "" -#: company/views.py:57 templates/navbar.html:25 +#: company/views.py:57 templates/navbar.html:32 msgid "Manufacturers" msgstr "" -#: company/views.py:64 templates/navbar.html:32 +#: company/views.py:64 templates/navbar.html:41 msgid "Customers" msgstr "" @@ -1330,15 +1330,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:404 part/views.py:2153 +#: company/views.py:404 part/views.py:2232 msgid "Added new price break" msgstr "" -#: company/views.py:441 part/views.py:2198 +#: company/views.py:441 part/views.py:2277 msgid "Edit Price Break" msgstr "" -#: company/views.py:456 part/views.py:2212 +#: company/views.py:456 part/views.py:2293 msgid "Delete Price Break" msgstr "" @@ -1431,7 +1431,7 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:259 part/views.py:1304 +#: order/models.py:185 order/models.py:259 part/views.py:1343 #: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "" @@ -1600,7 +1600,7 @@ msgid "Purchase Order Attachments" msgstr "" #: order/templates/order/po_tabs.html:8 order/templates/order/so_tabs.html:16 -#: part/templates/part/tabs.html:64 stock/templates/stock/tabs.html:32 +#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 msgid "Attachments" msgstr "" @@ -1616,7 +1616,7 @@ msgstr "" #: order/templates/order/purchase_order_detail.html:38 #: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:161 part/templates/part/category.html:202 +#: part/templates/part/category.html:171 part/templates/part/category.html:213 #: templates/js/stock.html:803 msgid "New Location" msgstr "" @@ -1658,7 +1658,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:135 templates/js/part.html:388 +#: part/templates/part/part_base.html:145 templates/js/part.html:388 msgid "On Order" msgstr "" @@ -1750,7 +1750,7 @@ msgstr "" msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:102 order/views.py:149 part/views.py:86 stock/views.py:167 +#: order/views.py:102 order/views.py:149 part/views.py:90 stock/views.py:167 msgid "Added attachment" msgstr "" @@ -1890,12 +1890,12 @@ msgstr "" msgid "Remove allocation" msgstr "" -#: part/bom.py:138 part/templates/part/category.html:55 +#: part/bom.py:138 part/templates/part/category.html:61 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "" -#: part/bom.py:139 part/templates/part/part_base.html:108 +#: part/bom.py:139 part/templates/part/part_base.html:118 msgid "Available Stock" msgstr "" @@ -2013,7 +2013,7 @@ msgid "Part Category" msgstr "" #: part/models.py:76 part/templates/part/category.html:18 -#: part/templates/part/category.html:83 templates/stats.html:12 +#: part/templates/part/category.html:89 templates/stats.html:12 msgid "Part Categories" msgstr "" @@ -2226,7 +2226,7 @@ msgstr "" msgid "BOM line checksum" msgstr "" -#: part/models.py:1612 part/views.py:1310 part/views.py:1362 +#: part/models.py:1612 part/views.py:1349 part/views.py:1401 #: stock/models.py:231 msgid "Quantity must be integer value for trackable parts" msgstr "" @@ -2285,23 +2285,23 @@ msgstr "" msgid "Finish Editing" msgstr "" -#: part/templates/part/bom.html:42 +#: part/templates/part/bom.html:43 msgid "Edit BOM" msgstr "" -#: part/templates/part/bom.html:44 +#: part/templates/part/bom.html:45 msgid "Validate Bill of Materials" msgstr "" -#: part/templates/part/bom.html:46 part/views.py:1597 +#: part/templates/part/bom.html:48 part/views.py:1640 msgid "Export Bill of Materials" msgstr "" -#: part/templates/part/bom.html:101 +#: part/templates/part/bom.html:103 msgid "Delete selected BOM items?" msgstr "" -#: part/templates/part/bom.html:102 +#: part/templates/part/bom.html:104 msgid "All selected BOM items will be deleted" msgstr "" @@ -2373,83 +2373,91 @@ msgstr "" msgid "Each part must already exist in the database" msgstr "" +#: part/templates/part/build.html:8 +msgid "Part Builds" +msgstr "" + +#: part/templates/part/build.html:14 +msgid "Start New Build" +msgstr "" + #: part/templates/part/category.html:19 msgid "All parts" msgstr "" -#: part/templates/part/category.html:23 part/views.py:1976 +#: part/templates/part/category.html:24 part/views.py:2043 msgid "Create new part category" msgstr "" -#: part/templates/part/category.html:27 +#: part/templates/part/category.html:30 msgid "Edit part category" msgstr "" -#: part/templates/part/category.html:30 +#: part/templates/part/category.html:35 msgid "Delete part category" msgstr "" -#: part/templates/part/category.html:39 part/templates/part/category.html:78 +#: part/templates/part/category.html:45 part/templates/part/category.html:84 msgid "Category Details" msgstr "" -#: part/templates/part/category.html:44 +#: part/templates/part/category.html:50 msgid "Category Path" msgstr "" -#: part/templates/part/category.html:49 +#: part/templates/part/category.html:55 msgid "Category Description" msgstr "" -#: part/templates/part/category.html:62 part/templates/part/detail.html:64 +#: part/templates/part/category.html:68 part/templates/part/detail.html:64 msgid "Keywords" msgstr "" -#: part/templates/part/category.html:68 +#: part/templates/part/category.html:74 msgid "Subcategories" msgstr "" -#: part/templates/part/category.html:73 +#: part/templates/part/category.html:79 msgid "Parts (Including subcategories)" msgstr "" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:112 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:107 part/views.py:491 +#: part/templates/part/category.html:114 part/views.py:511 msgid "Create new part" msgstr "" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 msgid "Set category" msgstr "" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 msgid "Set Category" msgstr "" -#: part/templates/part/category.html:113 +#: part/templates/part/category.html:123 msgid "Export Data" msgstr "" -#: part/templates/part/category.html:162 +#: part/templates/part/category.html:172 msgid "Create new location" msgstr "" -#: part/templates/part/category.html:167 part/templates/part/category.html:196 +#: part/templates/part/category.html:177 part/templates/part/category.html:207 msgid "New Category" msgstr "" -#: part/templates/part/category.html:168 +#: part/templates/part/category.html:178 msgid "Create new category" msgstr "" -#: part/templates/part/category.html:197 +#: part/templates/part/category.html:208 msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:203 stock/views.py:1314 +#: part/templates/part/category.html:214 stock/views.py:1314 msgid "Create new Stock Location" msgstr "" @@ -2461,7 +2469,7 @@ msgstr "" msgid "Part Details" msgstr "" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:85 +#: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 #: templates/js/part.html:112 msgid "IPN" msgstr "" @@ -2491,7 +2499,7 @@ msgstr "" msgid "Default Supplier" msgstr "" -#: part/templates/part/detail.html:102 part/templates/part/params.html:22 +#: part/templates/part/detail.html:102 part/templates/part/params.html:24 msgid "Units" msgstr "" @@ -2616,24 +2624,25 @@ msgstr "" msgid "Part Parameters" msgstr "" -#: part/templates/part/params.html:13 +#: part/templates/part/params.html:14 msgid "Add new parameter" msgstr "" -#: part/templates/part/params.html:13 templates/InvenTree/settings/part.html:12 +#: part/templates/part/params.html:14 templates/InvenTree/settings/part.html:12 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:21 stock/models.py:1391 +#: part/templates/part/params.html:23 stock/models.py:1391 #: templates/js/stock.html:112 msgid "Value" msgstr "" -#: part/templates/part/params.html:33 +#: part/templates/part/params.html:36 msgid "Edit" msgstr "" -#: part/templates/part/params.html:34 part/templates/part/supplier.html:17 +#: part/templates/part/params.html:39 part/templates/part/supplier.html:17 +#: users/models.py:141 msgid "Delete" msgstr "" @@ -2684,39 +2693,43 @@ msgstr "" msgid "Show pricing information" msgstr "" -#: part/templates/part/part_base.html:70 -msgid "Part actions" -msgstr "" - -#: part/templates/part/part_base.html:72 -msgid "Duplicate part" -msgstr "" - -#: part/templates/part/part_base.html:73 -msgid "Edit part" +#: part/templates/part/part_base.html:60 +msgid "Count part stock" msgstr "" #: part/templates/part/part_base.html:75 +msgid "Part actions" +msgstr "" + +#: part/templates/part/part_base.html:78 +msgid "Duplicate part" +msgstr "" + +#: part/templates/part/part_base.html:81 +msgid "Edit part" +msgstr "" + +#: part/templates/part/part_base.html:84 msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:114 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:124 templates/js/table_filters.html:65 msgid "In Stock" msgstr "" -#: part/templates/part/part_base.html:121 +#: part/templates/part/part_base.html:131 msgid "Allocated to Build Orders" msgstr "" -#: part/templates/part/part_base.html:128 +#: part/templates/part/part_base.html:138 msgid "Allocated to Sales Orders" msgstr "" -#: part/templates/part/part_base.html:150 +#: part/templates/part/part_base.html:160 msgid "Can Build" msgstr "" -#: part/templates/part/part_base.html:156 +#: part/templates/part/part_base.html:166 msgid "Underway" msgstr "" @@ -2736,7 +2749,7 @@ msgstr "" msgid "Upload new image" msgstr "" -#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:50 +#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:53 msgid "Sale Price" msgstr "" @@ -2797,11 +2810,11 @@ msgstr "" msgid "BOM" msgstr "" -#: part/templates/part/tabs.html:34 +#: part/templates/part/tabs.html:37 msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:282 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:282 msgid "Tests" msgstr "" @@ -2829,176 +2842,176 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:76 +#: part/views.py:78 msgid "Add part attachment" msgstr "" -#: part/views.py:125 templates/attachment_table.html:30 +#: part/views.py:129 templates/attachment_table.html:30 msgid "Edit attachment" msgstr "" -#: part/views.py:129 +#: part/views.py:135 msgid "Part attachment updated" msgstr "" -#: part/views.py:144 +#: part/views.py:150 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:150 +#: part/views.py:158 msgid "Deleted part attachment" msgstr "" -#: part/views.py:159 +#: part/views.py:167 msgid "Create Test Template" msgstr "" -#: part/views.py:186 +#: part/views.py:196 msgid "Edit Test Template" msgstr "" -#: part/views.py:200 +#: part/views.py:212 msgid "Delete Test Template" msgstr "" -#: part/views.py:207 +#: part/views.py:221 msgid "Set Part Category" msgstr "" -#: part/views.py:255 +#: part/views.py:271 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:290 +#: part/views.py:306 msgid "Create Variant" msgstr "" -#: part/views.py:368 +#: part/views.py:386 msgid "Duplicate Part" msgstr "" -#: part/views.py:373 +#: part/views.py:393 msgid "Copied part" msgstr "" -#: part/views.py:496 +#: part/views.py:518 msgid "Created new part" msgstr "" -#: part/views.py:707 +#: part/views.py:733 msgid "Part QR Code" msgstr "" -#: part/views.py:724 +#: part/views.py:752 msgid "Upload Part Image" msgstr "" -#: part/views.py:729 part/views.py:764 +#: part/views.py:760 part/views.py:797 msgid "Updated part image" msgstr "" -#: part/views.py:738 +#: part/views.py:769 msgid "Select Part Image" msgstr "" -#: part/views.py:767 +#: part/views.py:800 msgid "Part image not found" msgstr "" -#: part/views.py:778 +#: part/views.py:811 msgid "Edit Part Properties" msgstr "" -#: part/views.py:800 +#: part/views.py:835 msgid "Validate BOM" msgstr "" -#: part/views.py:963 +#: part/views.py:1002 msgid "No BOM file provided" msgstr "" -#: part/views.py:1313 +#: part/views.py:1352 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1338 part/views.py:1341 +#: part/views.py:1377 part/views.py:1380 msgid "Select valid part" msgstr "" -#: part/views.py:1347 +#: part/views.py:1386 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1385 +#: part/views.py:1424 msgid "Select a part" msgstr "" -#: part/views.py:1391 +#: part/views.py:1430 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1395 +#: part/views.py:1434 msgid "Specify quantity" msgstr "" -#: part/views.py:1645 +#: part/views.py:1690 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1652 +#: part/views.py:1699 msgid "Part was deleted" msgstr "" -#: part/views.py:1661 +#: part/views.py:1708 msgid "Part Pricing" msgstr "" -#: part/views.py:1783 +#: part/views.py:1834 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1791 +#: part/views.py:1844 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1798 +#: part/views.py:1853 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1806 +#: part/views.py:1863 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1856 +#: part/views.py:1915 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1870 +#: part/views.py:1931 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1927 +#: part/views.py:1990 msgid "Edit Part Category" msgstr "" -#: part/views.py:1962 +#: part/views.py:2027 msgid "Delete Part Category" msgstr "" -#: part/views.py:1968 +#: part/views.py:2035 msgid "Part category was deleted" msgstr "" -#: part/views.py:2027 +#: part/views.py:2098 msgid "Create BOM item" msgstr "" -#: part/views.py:2093 +#: part/views.py:2166 msgid "Edit BOM item" msgstr "" -#: part/views.py:2141 +#: part/views.py:2216 msgid "Confim BOM item deletion" msgstr "" @@ -3371,15 +3384,15 @@ msgid "Stock adjustment actions" msgstr "" #: stock/templates/stock/item_base.html:98 -#: stock/templates/stock/location.html:38 templates/stock_table.html:15 +#: stock/templates/stock/location.html:38 templates/stock_table.html:19 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:99 templates/stock_table.html:13 +#: stock/templates/stock/item_base.html:99 templates/stock_table.html:17 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:100 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:100 templates/stock_table.html:18 msgid "Remove stock" msgstr "" @@ -3819,6 +3832,14 @@ msgstr "" msgid "Add Stock Tracking Entry" msgstr "" +#: templates/403.html:5 templates/403.html:11 +msgid "Permission Denied" +msgstr "" + +#: templates/403.html:14 +msgid "You do not have permission to view this page." +msgstr "" + #: templates/InvenTree/bom_invalid.html:7 msgid "BOM Waiting Validation" msgstr "" @@ -3827,6 +3848,10 @@ msgstr "" msgid "Pending Builds" msgstr "" +#: templates/InvenTree/index.html:4 +msgid "Index" +msgstr "" + #: templates/InvenTree/latest_parts.html:7 msgid "Latest Parts" msgstr "" @@ -4392,39 +4417,39 @@ msgstr "" msgid "Purchasable" msgstr "" -#: templates/navbar.html:22 +#: templates/navbar.html:29 msgid "Buy" msgstr "" -#: templates/navbar.html:30 +#: templates/navbar.html:39 msgid "Sell" msgstr "" -#: templates/navbar.html:40 +#: templates/navbar.html:50 msgid "Scan Barcode" msgstr "" -#: templates/navbar.html:49 +#: templates/navbar.html:59 users/models.py:27 msgid "Admin" msgstr "" -#: templates/navbar.html:52 +#: templates/navbar.html:62 msgid "Settings" msgstr "" -#: templates/navbar.html:53 +#: templates/navbar.html:63 msgid "Logout" msgstr "" -#: templates/navbar.html:55 +#: templates/navbar.html:65 msgid "Login" msgstr "" -#: templates/navbar.html:58 +#: templates/navbar.html:68 msgid "About InvenTree" msgstr "" -#: templates/navbar.html:59 +#: templates/navbar.html:69 msgid "Statistics" msgstr "" @@ -4436,38 +4461,94 @@ msgstr "" msgid "Export Stock Information" msgstr "" -#: templates/stock_table.html:13 +#: templates/stock_table.html:17 msgid "Add to selected stock items" msgstr "" -#: templates/stock_table.html:14 +#: templates/stock_table.html:18 msgid "Remove from selected stock items" msgstr "" -#: templates/stock_table.html:15 +#: templates/stock_table.html:19 msgid "Stocktake selected stock items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move selected stock items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move stock" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order selected items" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order stock" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete selected items" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete Stock" msgstr "" + +#: users/admin.py:62 +msgid "Users" +msgstr "" + +#: users/admin.py:63 +msgid "Select which users are assigned to this group" +msgstr "" + +#: users/admin.py:124 +msgid "Personal info" +msgstr "" + +#: users/admin.py:125 +msgid "Permissions" +msgstr "" + +#: users/admin.py:128 +msgid "Important dates" +msgstr "" + +#: users/models.py:124 +msgid "Permission set" +msgstr "" + +#: users/models.py:132 +msgid "Group" +msgstr "" + +#: users/models.py:135 +msgid "View" +msgstr "" + +#: users/models.py:135 +msgid "Permission to view items" +msgstr "" + +#: users/models.py:137 +msgid "Create" +msgstr "" + +#: users/models.py:137 +msgid "Permission to add items" +msgstr "" + +#: users/models.py:139 +msgid "Update" +msgstr "" + +#: users/models.py:139 +msgid "Permissions to edit items" +msgstr "" + +#: users/models.py:141 +msgid "Permission to delete items" +msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index 0b5425db6e..0f38022f92 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-04 14:02+0000\n" +"POT-Creation-Date: 2020-10-05 13:20+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -90,7 +90,7 @@ msgstr "" msgid "User" msgstr "" -#: InvenTree/models.py:106 part/templates/part/params.html:20 +#: InvenTree/models.py:106 part/templates/part/params.html:22 #: templates/js/part.html:81 msgid "Name" msgstr "" @@ -99,19 +99,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:342 +#: InvenTree/settings.py:343 msgid "English" msgstr "" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:344 msgid "German" msgstr "" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:345 msgid "French" msgstr "" -#: InvenTree/settings.py:345 +#: InvenTree/settings.py:346 msgid "Polish" msgstr "" @@ -325,7 +325,7 @@ msgstr "" msgid "Number of parts to build" msgstr "" -#: build/models.py:128 part/templates/part/part_base.html:145 +#: build/models.py:128 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "" @@ -344,7 +344,7 @@ msgstr "" #: build/models.py:155 build/templates/build/detail.html:55 #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 -#: part/templates/part/detail.html:80 part/templates/part/part_base.html:92 +#: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 #: stock/models.py:381 stock/templates/stock/item_base.html:244 msgid "External Link" msgstr "" @@ -356,7 +356,7 @@ msgstr "" #: build/models.py:160 build/templates/build/tabs.html:14 company/models.py:310 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:15 #: order/templates/order/purchase_order_detail.html:202 -#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:67 +#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:70 #: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 #: stock/models.py:1404 stock/templates/stock/tabs.html:26 #: templates/js/barcode.html:391 templates/js/bom.html:223 @@ -404,7 +404,7 @@ msgstr "" #: build/templates/build/allocate.html:17 #: company/templates/company/detail_part.html:18 order/views.py:779 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order Parts" msgstr "" @@ -420,7 +420,7 @@ msgstr "" msgid "Unallocate" msgstr "" -#: build/templates/build/allocate.html:87 templates/stock_table.html:9 +#: build/templates/build/allocate.html:87 templates/stock_table.html:10 msgid "New Stock Item" msgstr "" @@ -548,7 +548,7 @@ msgstr "" #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 #: stock/templates/stock/item_base.html:223 templates/js/build.html:39 -#: templates/navbar.html:20 +#: templates/navbar.html:25 msgid "Build" msgstr "" @@ -687,7 +687,7 @@ msgstr "" #: build/templates/build/index.html:6 build/templates/build/index.html:14 #: order/templates/order/so_builds.html:11 order/templates/order/so_tabs.html:9 -#: part/templates/part/tabs.html:30 +#: part/templates/part/tabs.html:31 users/models.py:30 msgid "Build Orders" msgstr "" @@ -709,7 +709,7 @@ msgstr "" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 #: order/templates/order/order_notes.html:32 #: order/templates/order/sales_order_notes.html:37 -#: part/templates/part/notes.html:32 stock/templates/stock/item_notes.html:33 +#: part/templates/part/notes.html:33 stock/templates/stock/item_notes.html:33 msgid "Edit notes" msgstr "" @@ -1044,13 +1044,13 @@ msgid "New Supplier Part" msgstr "" #: company/templates/company/detail_part.html:15 -#: part/templates/part/category.html:109 part/templates/part/supplier.html:15 -#: templates/stock_table.html:11 +#: part/templates/part/category.html:117 part/templates/part/supplier.html:15 +#: templates/stock_table.html:14 msgid "Options" msgstr "" #: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order parts" msgstr "" @@ -1063,7 +1063,7 @@ msgid "Delete Parts" msgstr "" #: company/templates/company/detail_part.html:43 -#: part/templates/part/category.html:107 templates/js/stock.html:791 +#: part/templates/part/category.html:114 templates/js/stock.html:791 msgid "New Part" msgstr "" @@ -1095,7 +1095,7 @@ msgstr "" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:106 part/templates/part/category.html:113 +#: part/templates/part/category.html:112 part/templates/part/category.html:123 #: part/templates/part/stock.html:51 templates/stock_table.html:6 msgid "Export" msgstr "" @@ -1117,8 +1117,8 @@ msgstr "" #: company/templates/company/tabs.html:17 #: order/templates/order/purchase_orders.html:7 #: order/templates/order/purchase_orders.html:12 -#: part/templates/part/orders.html:9 part/templates/part/tabs.html:45 -#: templates/navbar.html:26 +#: part/templates/part/orders.html:9 part/templates/part/tabs.html:48 +#: templates/navbar.html:33 users/models.py:31 msgid "Purchase Orders" msgstr "" @@ -1136,8 +1136,8 @@ msgstr "" #: company/templates/company/tabs.html:22 #: order/templates/order/sales_orders.html:7 #: order/templates/order/sales_orders.html:12 -#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:53 -#: templates/navbar.html:33 +#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:56 +#: templates/navbar.html:42 users/models.py:32 msgid "Sales Orders" msgstr "" @@ -1158,7 +1158,7 @@ msgid "Supplier Part" msgstr "" #: company/templates/company/supplier_part_base.html:23 -#: part/templates/part/orders.html:14 +#: part/templates/part/orders.html:14 part/templates/part/part_base.html:66 msgid "Order part" msgstr "" @@ -1205,7 +1205,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2149 +#: part/templates/part/sale_prices.html:13 part/views.py:2226 msgid "Add Price Break" msgstr "" @@ -1241,7 +1241,7 @@ msgstr "" #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 #: templates/js/part.html:124 templates/js/part.html:372 -#: templates/js/stock.html:452 templates/navbar.html:19 +#: templates/js/stock.html:452 templates/navbar.html:22 users/models.py:29 msgid "Stock" msgstr "" @@ -1251,22 +1251,22 @@ msgstr "" #: company/templates/company/tabs.html:9 #: order/templates/order/receive_parts.html:14 part/models.py:294 -#: part/templates/part/cat_link.html:7 part/templates/part/category.html:88 -#: part/templates/part/category_tabs.html:6 templates/navbar.html:18 -#: templates/stats.html:8 templates/stats.html:17 +#: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 +#: part/templates/part/category_tabs.html:6 templates/navbar.html:19 +#: templates/stats.html:8 templates/stats.html:17 users/models.py:28 msgid "Parts" msgstr "" -#: company/views.py:50 part/templates/part/tabs.html:39 -#: templates/navbar.html:24 +#: company/views.py:50 part/templates/part/tabs.html:42 +#: templates/navbar.html:31 msgid "Suppliers" msgstr "" -#: company/views.py:57 templates/navbar.html:25 +#: company/views.py:57 templates/navbar.html:32 msgid "Manufacturers" msgstr "" -#: company/views.py:64 templates/navbar.html:32 +#: company/views.py:64 templates/navbar.html:41 msgid "Customers" msgstr "" @@ -1330,15 +1330,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:404 part/views.py:2153 +#: company/views.py:404 part/views.py:2232 msgid "Added new price break" msgstr "" -#: company/views.py:441 part/views.py:2198 +#: company/views.py:441 part/views.py:2277 msgid "Edit Price Break" msgstr "" -#: company/views.py:456 part/views.py:2212 +#: company/views.py:456 part/views.py:2293 msgid "Delete Price Break" msgstr "" @@ -1431,7 +1431,7 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:259 part/views.py:1304 +#: order/models.py:185 order/models.py:259 part/views.py:1343 #: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "" @@ -1600,7 +1600,7 @@ msgid "Purchase Order Attachments" msgstr "" #: order/templates/order/po_tabs.html:8 order/templates/order/so_tabs.html:16 -#: part/templates/part/tabs.html:64 stock/templates/stock/tabs.html:32 +#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 msgid "Attachments" msgstr "" @@ -1616,7 +1616,7 @@ msgstr "" #: order/templates/order/purchase_order_detail.html:38 #: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:161 part/templates/part/category.html:202 +#: part/templates/part/category.html:171 part/templates/part/category.html:213 #: templates/js/stock.html:803 msgid "New Location" msgstr "" @@ -1658,7 +1658,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:135 templates/js/part.html:388 +#: part/templates/part/part_base.html:145 templates/js/part.html:388 msgid "On Order" msgstr "" @@ -1750,7 +1750,7 @@ msgstr "" msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:102 order/views.py:149 part/views.py:86 stock/views.py:167 +#: order/views.py:102 order/views.py:149 part/views.py:90 stock/views.py:167 msgid "Added attachment" msgstr "" @@ -1890,12 +1890,12 @@ msgstr "" msgid "Remove allocation" msgstr "" -#: part/bom.py:138 part/templates/part/category.html:55 +#: part/bom.py:138 part/templates/part/category.html:61 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "" -#: part/bom.py:139 part/templates/part/part_base.html:108 +#: part/bom.py:139 part/templates/part/part_base.html:118 msgid "Available Stock" msgstr "" @@ -2013,7 +2013,7 @@ msgid "Part Category" msgstr "" #: part/models.py:76 part/templates/part/category.html:18 -#: part/templates/part/category.html:83 templates/stats.html:12 +#: part/templates/part/category.html:89 templates/stats.html:12 msgid "Part Categories" msgstr "" @@ -2226,7 +2226,7 @@ msgstr "" msgid "BOM line checksum" msgstr "" -#: part/models.py:1612 part/views.py:1310 part/views.py:1362 +#: part/models.py:1612 part/views.py:1349 part/views.py:1401 #: stock/models.py:231 msgid "Quantity must be integer value for trackable parts" msgstr "" @@ -2285,23 +2285,23 @@ msgstr "" msgid "Finish Editing" msgstr "" -#: part/templates/part/bom.html:42 +#: part/templates/part/bom.html:43 msgid "Edit BOM" msgstr "" -#: part/templates/part/bom.html:44 +#: part/templates/part/bom.html:45 msgid "Validate Bill of Materials" msgstr "" -#: part/templates/part/bom.html:46 part/views.py:1597 +#: part/templates/part/bom.html:48 part/views.py:1640 msgid "Export Bill of Materials" msgstr "" -#: part/templates/part/bom.html:101 +#: part/templates/part/bom.html:103 msgid "Delete selected BOM items?" msgstr "" -#: part/templates/part/bom.html:102 +#: part/templates/part/bom.html:104 msgid "All selected BOM items will be deleted" msgstr "" @@ -2373,83 +2373,91 @@ msgstr "" msgid "Each part must already exist in the database" msgstr "" +#: part/templates/part/build.html:8 +msgid "Part Builds" +msgstr "" + +#: part/templates/part/build.html:14 +msgid "Start New Build" +msgstr "" + #: part/templates/part/category.html:19 msgid "All parts" msgstr "" -#: part/templates/part/category.html:23 part/views.py:1976 +#: part/templates/part/category.html:24 part/views.py:2043 msgid "Create new part category" msgstr "" -#: part/templates/part/category.html:27 +#: part/templates/part/category.html:30 msgid "Edit part category" msgstr "" -#: part/templates/part/category.html:30 +#: part/templates/part/category.html:35 msgid "Delete part category" msgstr "" -#: part/templates/part/category.html:39 part/templates/part/category.html:78 +#: part/templates/part/category.html:45 part/templates/part/category.html:84 msgid "Category Details" msgstr "" -#: part/templates/part/category.html:44 +#: part/templates/part/category.html:50 msgid "Category Path" msgstr "" -#: part/templates/part/category.html:49 +#: part/templates/part/category.html:55 msgid "Category Description" msgstr "" -#: part/templates/part/category.html:62 part/templates/part/detail.html:64 +#: part/templates/part/category.html:68 part/templates/part/detail.html:64 msgid "Keywords" msgstr "" -#: part/templates/part/category.html:68 +#: part/templates/part/category.html:74 msgid "Subcategories" msgstr "" -#: part/templates/part/category.html:73 +#: part/templates/part/category.html:79 msgid "Parts (Including subcategories)" msgstr "" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:112 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:107 part/views.py:491 +#: part/templates/part/category.html:114 part/views.py:511 msgid "Create new part" msgstr "" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 msgid "Set category" msgstr "" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 msgid "Set Category" msgstr "" -#: part/templates/part/category.html:113 +#: part/templates/part/category.html:123 msgid "Export Data" msgstr "" -#: part/templates/part/category.html:162 +#: part/templates/part/category.html:172 msgid "Create new location" msgstr "" -#: part/templates/part/category.html:167 part/templates/part/category.html:196 +#: part/templates/part/category.html:177 part/templates/part/category.html:207 msgid "New Category" msgstr "" -#: part/templates/part/category.html:168 +#: part/templates/part/category.html:178 msgid "Create new category" msgstr "" -#: part/templates/part/category.html:197 +#: part/templates/part/category.html:208 msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:203 stock/views.py:1314 +#: part/templates/part/category.html:214 stock/views.py:1314 msgid "Create new Stock Location" msgstr "" @@ -2461,7 +2469,7 @@ msgstr "" msgid "Part Details" msgstr "" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:85 +#: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 #: templates/js/part.html:112 msgid "IPN" msgstr "" @@ -2491,7 +2499,7 @@ msgstr "" msgid "Default Supplier" msgstr "" -#: part/templates/part/detail.html:102 part/templates/part/params.html:22 +#: part/templates/part/detail.html:102 part/templates/part/params.html:24 msgid "Units" msgstr "" @@ -2616,24 +2624,25 @@ msgstr "" msgid "Part Parameters" msgstr "" -#: part/templates/part/params.html:13 +#: part/templates/part/params.html:14 msgid "Add new parameter" msgstr "" -#: part/templates/part/params.html:13 templates/InvenTree/settings/part.html:12 +#: part/templates/part/params.html:14 templates/InvenTree/settings/part.html:12 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:21 stock/models.py:1391 +#: part/templates/part/params.html:23 stock/models.py:1391 #: templates/js/stock.html:112 msgid "Value" msgstr "" -#: part/templates/part/params.html:33 +#: part/templates/part/params.html:36 msgid "Edit" msgstr "" -#: part/templates/part/params.html:34 part/templates/part/supplier.html:17 +#: part/templates/part/params.html:39 part/templates/part/supplier.html:17 +#: users/models.py:141 msgid "Delete" msgstr "" @@ -2684,39 +2693,43 @@ msgstr "" msgid "Show pricing information" msgstr "" -#: part/templates/part/part_base.html:70 -msgid "Part actions" -msgstr "" - -#: part/templates/part/part_base.html:72 -msgid "Duplicate part" -msgstr "" - -#: part/templates/part/part_base.html:73 -msgid "Edit part" +#: part/templates/part/part_base.html:60 +msgid "Count part stock" msgstr "" #: part/templates/part/part_base.html:75 +msgid "Part actions" +msgstr "" + +#: part/templates/part/part_base.html:78 +msgid "Duplicate part" +msgstr "" + +#: part/templates/part/part_base.html:81 +msgid "Edit part" +msgstr "" + +#: part/templates/part/part_base.html:84 msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:114 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:124 templates/js/table_filters.html:65 msgid "In Stock" msgstr "" -#: part/templates/part/part_base.html:121 +#: part/templates/part/part_base.html:131 msgid "Allocated to Build Orders" msgstr "" -#: part/templates/part/part_base.html:128 +#: part/templates/part/part_base.html:138 msgid "Allocated to Sales Orders" msgstr "" -#: part/templates/part/part_base.html:150 +#: part/templates/part/part_base.html:160 msgid "Can Build" msgstr "" -#: part/templates/part/part_base.html:156 +#: part/templates/part/part_base.html:166 msgid "Underway" msgstr "" @@ -2736,7 +2749,7 @@ msgstr "" msgid "Upload new image" msgstr "" -#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:50 +#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:53 msgid "Sale Price" msgstr "" @@ -2797,11 +2810,11 @@ msgstr "" msgid "BOM" msgstr "" -#: part/templates/part/tabs.html:34 +#: part/templates/part/tabs.html:37 msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:282 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:282 msgid "Tests" msgstr "" @@ -2829,176 +2842,176 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:76 +#: part/views.py:78 msgid "Add part attachment" msgstr "" -#: part/views.py:125 templates/attachment_table.html:30 +#: part/views.py:129 templates/attachment_table.html:30 msgid "Edit attachment" msgstr "" -#: part/views.py:129 +#: part/views.py:135 msgid "Part attachment updated" msgstr "" -#: part/views.py:144 +#: part/views.py:150 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:150 +#: part/views.py:158 msgid "Deleted part attachment" msgstr "" -#: part/views.py:159 +#: part/views.py:167 msgid "Create Test Template" msgstr "" -#: part/views.py:186 +#: part/views.py:196 msgid "Edit Test Template" msgstr "" -#: part/views.py:200 +#: part/views.py:212 msgid "Delete Test Template" msgstr "" -#: part/views.py:207 +#: part/views.py:221 msgid "Set Part Category" msgstr "" -#: part/views.py:255 +#: part/views.py:271 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:290 +#: part/views.py:306 msgid "Create Variant" msgstr "" -#: part/views.py:368 +#: part/views.py:386 msgid "Duplicate Part" msgstr "" -#: part/views.py:373 +#: part/views.py:393 msgid "Copied part" msgstr "" -#: part/views.py:496 +#: part/views.py:518 msgid "Created new part" msgstr "" -#: part/views.py:707 +#: part/views.py:733 msgid "Part QR Code" msgstr "" -#: part/views.py:724 +#: part/views.py:752 msgid "Upload Part Image" msgstr "" -#: part/views.py:729 part/views.py:764 +#: part/views.py:760 part/views.py:797 msgid "Updated part image" msgstr "" -#: part/views.py:738 +#: part/views.py:769 msgid "Select Part Image" msgstr "" -#: part/views.py:767 +#: part/views.py:800 msgid "Part image not found" msgstr "" -#: part/views.py:778 +#: part/views.py:811 msgid "Edit Part Properties" msgstr "" -#: part/views.py:800 +#: part/views.py:835 msgid "Validate BOM" msgstr "" -#: part/views.py:963 +#: part/views.py:1002 msgid "No BOM file provided" msgstr "" -#: part/views.py:1313 +#: part/views.py:1352 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1338 part/views.py:1341 +#: part/views.py:1377 part/views.py:1380 msgid "Select valid part" msgstr "" -#: part/views.py:1347 +#: part/views.py:1386 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1385 +#: part/views.py:1424 msgid "Select a part" msgstr "" -#: part/views.py:1391 +#: part/views.py:1430 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1395 +#: part/views.py:1434 msgid "Specify quantity" msgstr "" -#: part/views.py:1645 +#: part/views.py:1690 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1652 +#: part/views.py:1699 msgid "Part was deleted" msgstr "" -#: part/views.py:1661 +#: part/views.py:1708 msgid "Part Pricing" msgstr "" -#: part/views.py:1783 +#: part/views.py:1834 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1791 +#: part/views.py:1844 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1798 +#: part/views.py:1853 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1806 +#: part/views.py:1863 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1856 +#: part/views.py:1915 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1870 +#: part/views.py:1931 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1927 +#: part/views.py:1990 msgid "Edit Part Category" msgstr "" -#: part/views.py:1962 +#: part/views.py:2027 msgid "Delete Part Category" msgstr "" -#: part/views.py:1968 +#: part/views.py:2035 msgid "Part category was deleted" msgstr "" -#: part/views.py:2027 +#: part/views.py:2098 msgid "Create BOM item" msgstr "" -#: part/views.py:2093 +#: part/views.py:2166 msgid "Edit BOM item" msgstr "" -#: part/views.py:2141 +#: part/views.py:2216 msgid "Confim BOM item deletion" msgstr "" @@ -3371,15 +3384,15 @@ msgid "Stock adjustment actions" msgstr "" #: stock/templates/stock/item_base.html:98 -#: stock/templates/stock/location.html:38 templates/stock_table.html:15 +#: stock/templates/stock/location.html:38 templates/stock_table.html:19 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:99 templates/stock_table.html:13 +#: stock/templates/stock/item_base.html:99 templates/stock_table.html:17 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:100 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:100 templates/stock_table.html:18 msgid "Remove stock" msgstr "" @@ -3819,6 +3832,14 @@ msgstr "" msgid "Add Stock Tracking Entry" msgstr "" +#: templates/403.html:5 templates/403.html:11 +msgid "Permission Denied" +msgstr "" + +#: templates/403.html:14 +msgid "You do not have permission to view this page." +msgstr "" + #: templates/InvenTree/bom_invalid.html:7 msgid "BOM Waiting Validation" msgstr "" @@ -3827,6 +3848,10 @@ msgstr "" msgid "Pending Builds" msgstr "" +#: templates/InvenTree/index.html:4 +msgid "Index" +msgstr "" + #: templates/InvenTree/latest_parts.html:7 msgid "Latest Parts" msgstr "" @@ -4392,39 +4417,39 @@ msgstr "" msgid "Purchasable" msgstr "" -#: templates/navbar.html:22 +#: templates/navbar.html:29 msgid "Buy" msgstr "" -#: templates/navbar.html:30 +#: templates/navbar.html:39 msgid "Sell" msgstr "" -#: templates/navbar.html:40 +#: templates/navbar.html:50 msgid "Scan Barcode" msgstr "" -#: templates/navbar.html:49 +#: templates/navbar.html:59 users/models.py:27 msgid "Admin" msgstr "" -#: templates/navbar.html:52 +#: templates/navbar.html:62 msgid "Settings" msgstr "" -#: templates/navbar.html:53 +#: templates/navbar.html:63 msgid "Logout" msgstr "" -#: templates/navbar.html:55 +#: templates/navbar.html:65 msgid "Login" msgstr "" -#: templates/navbar.html:58 +#: templates/navbar.html:68 msgid "About InvenTree" msgstr "" -#: templates/navbar.html:59 +#: templates/navbar.html:69 msgid "Statistics" msgstr "" @@ -4436,38 +4461,94 @@ msgstr "" msgid "Export Stock Information" msgstr "" -#: templates/stock_table.html:13 +#: templates/stock_table.html:17 msgid "Add to selected stock items" msgstr "" -#: templates/stock_table.html:14 +#: templates/stock_table.html:18 msgid "Remove from selected stock items" msgstr "" -#: templates/stock_table.html:15 +#: templates/stock_table.html:19 msgid "Stocktake selected stock items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move selected stock items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move stock" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order selected items" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order stock" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete selected items" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete Stock" msgstr "" + +#: users/admin.py:62 +msgid "Users" +msgstr "" + +#: users/admin.py:63 +msgid "Select which users are assigned to this group" +msgstr "" + +#: users/admin.py:124 +msgid "Personal info" +msgstr "" + +#: users/admin.py:125 +msgid "Permissions" +msgstr "" + +#: users/admin.py:128 +msgid "Important dates" +msgstr "" + +#: users/models.py:124 +msgid "Permission set" +msgstr "" + +#: users/models.py:132 +msgid "Group" +msgstr "" + +#: users/models.py:135 +msgid "View" +msgstr "" + +#: users/models.py:135 +msgid "Permission to view items" +msgstr "" + +#: users/models.py:137 +msgid "Create" +msgstr "" + +#: users/models.py:137 +msgid "Permission to add items" +msgstr "" + +#: users/models.py:139 +msgid "Update" +msgstr "" + +#: users/models.py:139 +msgid "Permissions to edit items" +msgstr "" + +#: users/models.py:141 +msgid "Permission to delete items" +msgstr "" diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index a7915878c5..4a9dbfa2ac 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -6,7 +6,7 @@ JSON API for the Order app from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import generics, permissions +from rest_framework import generics from rest_framework import filters from django.conf.urls import url, include @@ -109,10 +109,6 @@ class POList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -162,10 +158,6 @@ class PODetail(generics.RetrieveUpdateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated - ] - class POLineItemList(generics.ListCreateAPIView): """ API endpoint for accessing a list of POLineItem objects @@ -188,10 +180,6 @@ class POLineItemList(generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, ] @@ -208,10 +196,6 @@ class POLineItemDetail(generics.RetrieveUpdateAPIView): queryset = PurchaseOrderLineItem serializer_class = POLineItemSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ @@ -300,10 +284,6 @@ class SOList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -351,8 +331,6 @@ class SODetail(generics.RetrieveUpdateAPIView): return queryset - permission_classes = [permissions.IsAuthenticated] - class SOLineItemList(generics.ListCreateAPIView): """ @@ -398,8 +376,6 @@ class SOLineItemList(generics.ListCreateAPIView): return queryset - permission_classes = [permissions.IsAuthenticated] - filter_backends = [DjangoFilterBackend] filter_fields = [ @@ -414,8 +390,6 @@ class SOLineItemDetail(generics.RetrieveUpdateAPIView): queryset = SalesOrderLineItem.objects.all() serializer_class = SOLineItemSerializer - permission_classes = [permissions.IsAuthenticated] - class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 036021b12d..71e3455722 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -24,7 +24,7 @@ src="{% static 'img/blank_image.png' %}"

{{ order }} - {% if user.is_staff and perms.order.change_purchaseorder %} + {% if user.is_staff and roles.purchase_order.change %} {% endif %}

diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 70505ddccc..0572104e09 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -34,7 +34,7 @@ src="{% static 'img/blank_image.png' %}"

{{ order }} - {% if user.is_staff and perms.order.change_salesorder %} + {% if user.is_staff and roles.sales_order.change %} {% endif %}

diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 6834503466..d643b8671a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -44,6 +44,10 @@ class PartCategoryTree(TreeSerializer): def get_items(self): return PartCategory.objects.all().prefetch_related('parts', 'children') + permission_classes = [ + permissions.IsAuthenticated, + ] + class CategoryList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartCategory objects. @@ -55,10 +59,6 @@ class CategoryList(generics.ListCreateAPIView): queryset = PartCategory.objects.all() serializer_class = part_serializers.CategorySerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - def get_queryset(self): """ Custom filtering: @@ -119,10 +119,6 @@ class PartSalePriceList(generics.ListCreateAPIView): queryset = PartSellPriceBreak.objects.all() serializer_class = part_serializers.PartSalePriceSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend ] @@ -182,8 +178,6 @@ class PartTestTemplateList(generics.ListCreateAPIView): return queryset - permission_classes = [permissions.IsAuthenticated] - filter_backends = [ DjangoFilterBackend, filters.OrderingFilter, @@ -221,10 +215,6 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView): queryset = Part.objects.all() serializer_class = part_serializers.PartThumbSerializerUpdate - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend ] @@ -246,10 +236,6 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - def get_serializer(self, *args, **kwargs): try: @@ -580,10 +566,6 @@ class PartList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -676,10 +658,6 @@ class PartParameterTemplateList(generics.ListCreateAPIView): queryset = PartParameterTemplate.objects.all() serializer_class = part_serializers.PartParameterTemplateSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ filters.OrderingFilter, ] @@ -699,10 +677,6 @@ class PartParameterList(generics.ListCreateAPIView): queryset = PartParameter.objects.all() serializer_class = part_serializers.PartParameterSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend ] @@ -796,10 +770,6 @@ class BomList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -816,10 +786,6 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView): queryset = BomItem.objects.all() serializer_class = part_serializers.BomItemSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - class BomItemValidate(generics.UpdateAPIView): """ API endpoint for validating a BomItem """ diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 2653a42575..c996edc2db 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -39,10 +39,12 @@ {% elif part.active %} + {% if roles.part.change %} {% if part.is_bom_valid == False %} {% endif %} + {% endif %} {% endif %} diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index 442693ddeb..bfd72a2f70 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -1,15 +1,18 @@ {% extends "part/part_base.html" %} {% load static %} +{% load i18n %} {% block details %} {% include 'part/tabs.html' with tab='build' %} -

Part Builds

+

{% trans "Part Builds" %}

{% if part.active %} - + {% if roles.build.add %} + + {% endif %} {% endif %}
diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 9a6fb9d7e5..d73aba0291 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -9,7 +9,7 @@ {% if category %}

{{ category.name }} - {% if user.is_staff and perms.part.change_partcategory %} + {% if user.is_staff and roles.part.change %} {% endif %}

@@ -20,17 +20,23 @@ {% endif %}

+ {% if roles.part.add %} + {% endif %} {% if category %} + {% if roles.part.change %} + {% endif %} + {% if roles.part.delete %} {% endif %} + {% endif %}

@@ -104,11 +110,15 @@
+ {% if roles.part.add %} + {% endif %}
@@ -180,6 +190,7 @@ location.href = url; }); + {% if roles.part.add %} $("#part-create").click(function() { launchModalForm( "{% url 'part-create' %}", @@ -207,6 +218,7 @@ } ); }); + {% endif %} {% if category %} $("#cat-edit").click(function () { diff --git a/InvenTree/part/templates/part/notes.html b/InvenTree/part/templates/part/notes.html index afa215600d..3f833325cd 100644 --- a/InvenTree/part/templates/part/notes.html +++ b/InvenTree/part/templates/part/notes.html @@ -29,7 +29,9 @@

{% trans "Part Notes" %}

+ {% if roles.part.change %} + {% endif %}

diff --git a/InvenTree/part/templates/part/params.html b/InvenTree/part/templates/part/params.html index 8ad4f61252..ba8aa0566d 100644 --- a/InvenTree/part/templates/part/params.html +++ b/InvenTree/part/templates/part/params.html @@ -10,7 +10,9 @@
+ {% if roles.part.add %} + {% endif %}
@@ -30,8 +32,12 @@ {{ param.template.units }}
+ {% if roles.part.change %} + {% endif %} + {% if roles.part.delete %} + {% endif %}
@@ -48,6 +54,7 @@ $('#param-table').inventreeTable({ }); + {% if roles.part.add %} $('#param-create').click(function() { launchModalForm("{% url 'part-param-create' %}?part={{ part.id }}", { reload: true, @@ -59,6 +66,7 @@ }], }); }); + {% endif %} $('.param-edit').click(function() { var button = $(this); diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 1de4976740..d7604deae4 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -28,7 +28,7 @@

{{ part.full_name }} - {% if user.is_staff and perms.part.change_part %} + {% if user.is_staff and roles.part.change %} {% endif %} {% if not part.active %} @@ -56,26 +56,36 @@ - + {% endif %} {% if part.purchaseable %} - {% endif %} {% endif %} + {% endif %} + {% if roles.part.add or roles.part.change or roles.part.delete %}
+ {% endif %}

@@ -274,6 +284,7 @@ }); }); + {% if roles.part.change %} $("#part-edit").click(function() { launchModalForm( "{% url 'part-edit' part.id %}", @@ -282,6 +293,7 @@ } ); }); + {% endif %} $("#part-order").click(function() { launchModalForm("{% url 'order-parts' %}", { @@ -292,6 +304,7 @@ }); }); + {% if roles.part.add %} $("#part-duplicate").click(function() { launchModalForm( "{% url 'part-duplicate' part.id %}", @@ -300,8 +313,9 @@ } ); }); + {% endif %} - {% if not part.active %} + {% if not part.active and roles.part.delete %} $("#part-delete").click(function() { launchModalForm( "{% url 'part-delete' part.id %}", diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 1eab299ed5..8322a225bc 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -26,14 +26,17 @@ {% if part.assembly %} {% trans "BOM" %}{{ part.bom_count }} + {% if roles.build.view %} - {% trans "Build Orders" %}{{ part.builds.count }} + {% trans "Build Orders" %}{{ part.builds.count }} + + {% endif %} {% endif %} {% if part.component or part.used_in_count > 0 %} {% trans "Used In" %} {% if part.used_in_count > 0 %}{{ part.used_in_count }}{% endif %} {% endif %} - {% if part.purchaseable %} + {% if part.purchaseable and roles.purchase_order.view %} {% if part.is_template == False %} {% trans "Suppliers" %} @@ -45,7 +48,7 @@ {% trans "Purchase Orders" %} {{ part.purchase_orders|length }} {% endif %} - {% if part.salable %} + {% if part.salable and roles.sales_order.view %}
  • {% trans "Sale Price" %}
  • diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 3b116fa445..328ad3ef9e 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -3,6 +3,7 @@ from rest_framework import status from django.urls import reverse from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group from part.models import Part from stock.models import StockItem @@ -29,7 +30,26 @@ class PartAPITest(APITestCase): def setUp(self): # Create a user for auth User = get_user_model() - User.objects.create_user('testuser', 'test@testing.com', 'password') + self.user = User.objects.create_user( + username='testuser', + email='test@testing.com', + password='password' + ) + + # Put the user into a group with the correct permissions + group = Group.objects.create(name='mygroup') + self.user.groups.add(group) + + # Give the group *all* the permissions! + for rule in group.rule_sets.all(): + rule.can_view = True + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() + + group.save() self.client.login(username='testuser', password='password') diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index bc09784a47..d8c345d243 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -3,6 +3,7 @@ from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group from .models import Part @@ -23,7 +24,24 @@ class PartViewTestCase(TestCase): # Create a user User = get_user_model() - User.objects.create_user('username', 'user@email.com', 'password') + self.user = User.objects.create_user( + username='username', + email='user@email.com', + password='password' + ) + + # Put the user into a group with the correct permissions + group = Group.objects.create(name='mygroup') + self.user.groups.add(group) + + # Give the group *all* the permissions! + for rule in group.rule_sets.all(): + rule.can_view = True + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() self.client.login(username='username', password='password') @@ -140,12 +158,14 @@ class PartTests(PartViewTestCase): """ Tests for Part forms """ def test_part_edit(self): + response = self.client.get(reverse('part-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) keys = response.context.keys() data = str(response.content) + self.assertEqual(response.status_code, 200) + self.assertIn('part', keys) self.assertIn('csrf_token', keys) @@ -189,6 +209,8 @@ class PartAttachmentTests(PartViewTestCase): response = self.client.get(reverse('part-attachment-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) + # TODO - Create a new attachment using this view + def test_invalid_create(self): """ test creation of an attachment for an invalid part """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index dc8d07f5cd..6498774285 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -38,17 +38,21 @@ from .admin import PartResource from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView +from InvenTree.views import InvenTreeRoleMixin from InvenTree.helpers import DownloadFile, str2bool -class PartIndex(ListView): +class PartIndex(InvenTreeRoleMixin, ListView): """ View for displaying list of Part objects """ + model = Part template_name = 'part/category.html' context_object_name = 'parts' + role_required = 'part.view' + def get_queryset(self): return Part.objects.all().select_related('category') @@ -76,6 +80,8 @@ class PartAttachmentCreate(AjaxCreateView): ajax_form_title = _("Add part attachment") ajax_template_name = "modal_form.html" + role_required = 'part.add' + def post_save(self): """ Record the user that uploaded the attachment """ self.object.user = self.request.user @@ -123,6 +129,8 @@ class PartAttachmentEdit(AjaxUpdateView): form_class = part_forms.EditPartAttachmentForm ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit attachment') + + role_required = 'part.change' def get_data(self): return { @@ -145,6 +153,8 @@ class PartAttachmentDelete(AjaxDeleteView): ajax_template_name = "attachment_delete.html" context_object_name = "attachment" + role_required = 'part.delete' + def get_data(self): return { 'danger': _('Deleted part attachment') @@ -157,6 +167,8 @@ class PartTestTemplateCreate(AjaxCreateView): model = PartTestTemplate form_class = part_forms.EditPartTestTemplateForm ajax_form_title = _("Create Test Template") + + role_required = 'part.add' def get_initial(self): @@ -185,6 +197,8 @@ class PartTestTemplateEdit(AjaxUpdateView): form_class = part_forms.EditPartTestTemplateForm ajax_form_title = _("Edit Test Template") + role_required = 'part.change' + def get_form(self): form = super().get_form() @@ -199,6 +213,8 @@ class PartTestTemplateDelete(AjaxDeleteView): model = PartTestTemplate ajax_form_title = _("Delete Test Template") + role_required = 'part.delete' + class PartSetCategory(AjaxUpdateView): """ View for settings the part category for multiple parts at once """ @@ -207,6 +223,8 @@ class PartSetCategory(AjaxUpdateView): ajax_form_title = _('Set Part Category') form_class = part_forms.SetPartCategoryForm + role_required = 'part.change' + category = None parts = [] @@ -290,6 +308,8 @@ class MakePartVariant(AjaxCreateView): ajax_form_title = _('Create Variant') ajax_template_name = 'part/variant_part.html' + role_required = 'part.add' + def get_part_template(self): return get_object_or_404(Part, id=self.kwargs['pk']) @@ -368,6 +388,8 @@ class PartDuplicate(AjaxCreateView): ajax_form_title = _("Duplicate Part") ajax_template_name = "part/copy_part.html" + role_required = 'part.add' + def get_data(self): return { 'success': _('Copied part') @@ -491,6 +513,8 @@ class PartCreate(AjaxCreateView): ajax_form_title = _('Create new part') ajax_template_name = 'part/create_part.html' + role_required = 'part.add' + def get_data(self): return { 'success': _("Created new part"), @@ -613,6 +637,8 @@ class PartNotes(UpdateView): template_name = 'part/notes.html' model = Part + role_required = 'part.change' + fields = ['notes'] def get_success_url(self): @@ -634,7 +660,7 @@ class PartNotes(UpdateView): return ctx -class PartDetail(DetailView): +class PartDetail(InvenTreeRoleMixin, DetailView): """ Detail view for Part object """ @@ -642,6 +668,8 @@ class PartDetail(DetailView): queryset = Part.objects.all().select_related('category') template_name = 'part/detail.html' + role_required = 'part.view' + # Add in some extra context information based on query params def get_context_data(self, **kwargs): """ Provide extra context data to template @@ -706,6 +734,8 @@ class PartQRCode(QRCodeView): ajax_form_title = _("Part QR Code") + role_required = 'part.view' + def get_qr_data(self): """ Generate QR code data for the Part """ @@ -722,8 +752,11 @@ class PartImageUpload(AjaxUpdateView): model = Part ajax_template_name = 'modal_form.html' ajax_form_title = _('Upload Part Image') + form_class = part_forms.PartImageForm + role_required = 'part.change' + def get_data(self): return { 'success': _('Updated part image'), @@ -737,6 +770,8 @@ class PartImageSelect(AjaxUpdateView): ajax_template_name = 'part/select_image.html' ajax_form_title = _('Select Part Image') + role_required = 'part.change' + fields = [ 'image', ] @@ -778,6 +813,8 @@ class PartEdit(AjaxUpdateView): ajax_form_title = _('Edit Part Properties') context_object_name = 'part' + role_required = 'part.change' + def get_form(self): """ Create form for Part editing. Overrides default get_form() method to limit the choices @@ -802,6 +839,8 @@ class BomValidate(AjaxUpdateView): context_object_name = 'part' form_class = part_forms.BomValidateForm + role_required = 'part.change' + def get_context(self): return { 'part': self.get_object(), @@ -832,7 +871,7 @@ class BomValidate(AjaxUpdateView): return self.renderJsonResponse(request, form, data, context=self.get_context()) -class BomUpload(FormView): +class BomUpload(InvenTreeRoleMixin, FormView): """ View for uploading a BOM file, and handling BOM data importing. The BOM upload process is as follows: @@ -868,6 +907,8 @@ class BomUpload(FormView): missing_columns = [] allowed_parts = [] + role_required = ('part.change', 'part.add') + def get_success_url(self): part = self.get_object() return reverse('upload-bom', kwargs={'pk': part.id}) @@ -1466,6 +1507,8 @@ class BomUpload(FormView): class PartExport(AjaxView): """ Export a CSV file containing information on multiple parts """ + role_required = 'part.view' + def get_parts(self, request): """ Extract part list from the POST parameters. Parts can be supplied as: @@ -1543,6 +1586,8 @@ class BomDownload(AjaxView): - File format should be passed as a query param e.g. ?format=csv """ + role_required = 'part.view' + model = Part def get(self, request, *args, **kwargs): @@ -1596,6 +1641,8 @@ class BomExport(AjaxView): form_class = part_forms.BomExportForm ajax_form_title = _("Export Bill of Materials") + role_required = 'part.view' + def get(self, request, *args, **kwargs): return self.renderJsonResponse(request, self.form_class()) @@ -1645,6 +1692,8 @@ class PartDelete(AjaxDeleteView): ajax_form_title = _('Confirm Part Deletion') context_object_name = 'part' + role_required = 'part.delete' + success_url = '/part/' def get_data(self): @@ -1661,6 +1710,8 @@ class PartPricing(AjaxView): ajax_form_title = _("Part Pricing") form_class = part_forms.PartPriceForm + role_required = ['sales_order.view', 'part.view'] + def get_part(self): try: return Part.objects.get(id=self.kwargs['pk']) @@ -1778,6 +1829,8 @@ class PartPricing(AjaxView): class PartParameterTemplateCreate(AjaxCreateView): """ View for creating a new PartParameterTemplate """ + role_required = 'part.add' + model = PartParameterTemplate form_class = part_forms.EditPartParameterTemplateForm ajax_form_title = _('Create Part Parameter Template') @@ -1786,6 +1839,8 @@ class PartParameterTemplateCreate(AjaxCreateView): class PartParameterTemplateEdit(AjaxUpdateView): """ View for editing a PartParameterTemplate """ + role_required = 'part.change' + model = PartParameterTemplate form_class = part_forms.EditPartParameterTemplateForm ajax_form_title = _('Edit Part Parameter Template') @@ -1794,6 +1849,8 @@ class PartParameterTemplateEdit(AjaxUpdateView): class PartParameterTemplateDelete(AjaxDeleteView): """ View for deleting an existing PartParameterTemplate """ + role_required = 'part.delete' + model = PartParameterTemplate ajax_form_title = _("Delete Part Parameter Template") @@ -1801,6 +1858,8 @@ class PartParameterTemplateDelete(AjaxDeleteView): class PartParameterCreate(AjaxCreateView): """ View for creating a new PartParameter """ + role_required = 'part.add' + model = PartParameter form_class = part_forms.EditPartParameterForm ajax_form_title = _('Create Part Parameter') @@ -1851,6 +1910,8 @@ class PartParameterCreate(AjaxCreateView): class PartParameterEdit(AjaxUpdateView): """ View for editing a PartParameter """ + role_required = 'part.change' + model = PartParameter form_class = part_forms.EditPartParameterForm ajax_form_title = _('Edit Part Parameter') @@ -1865,12 +1926,14 @@ class PartParameterEdit(AjaxUpdateView): class PartParameterDelete(AjaxDeleteView): """ View for deleting a PartParameter """ + role_required = 'part.delete' + model = PartParameter ajax_template_name = 'part/param_delete.html' ajax_form_title = _('Delete Part Parameter') -class CategoryDetail(DetailView): +class CategoryDetail(InvenTreeRoleMixin, DetailView): """ Detail view for PartCategory """ model = PartCategory @@ -1878,6 +1941,8 @@ class CategoryDetail(DetailView): queryset = PartCategory.objects.all().prefetch_related('children') template_name = 'part/category_partlist.html' + role_required = 'part.view' + def get_context_data(self, **kwargs): context = super(CategoryDetail, self).get_context_data(**kwargs).copy() @@ -1926,6 +1991,8 @@ class CategoryEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit Part Category') + role_required = 'part.change' + def get_context_data(self, **kwargs): context = super(CategoryEdit, self).get_context_data(**kwargs).copy() @@ -1963,6 +2030,8 @@ class CategoryDelete(AjaxDeleteView): context_object_name = 'category' success_url = '/part/' + role_required = 'part.delete' + def get_data(self): return { 'danger': _('Part category was deleted'), @@ -1977,6 +2046,8 @@ class CategoryCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' form_class = part_forms.EditCategoryForm + role_required = 'part.add' + def get_context_data(self, **kwargs): """ Add extra context data to template. @@ -2012,12 +2083,14 @@ class CategoryCreate(AjaxCreateView): return initials -class BomItemDetail(DetailView): +class BomItemDetail(InvenTreeRoleMixin, DetailView): """ Detail view for BomItem """ context_object_name = 'item' queryset = BomItem.objects.all() template_name = 'part/bom-detail.html' + role_required = 'part.view' + class BomItemCreate(AjaxCreateView): """ Create view for making a new BomItem object """ @@ -2026,6 +2099,8 @@ class BomItemCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Create BOM item') + role_required = 'part.add' + def get_form(self): """ Override get_form() method to reduce Part selection options. @@ -2092,6 +2167,8 @@ class BomItemEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit BOM item') + role_required = 'part.change' + def get_form(self): """ Override get_form() method to filter part selection options @@ -2140,6 +2217,8 @@ class BomItemDelete(AjaxDeleteView): context_object_name = 'item' ajax_form_title = _('Confim BOM item deletion') + role_required = 'part.delete' + class PartSalePriceBreakCreate(AjaxCreateView): """ View for creating a sale price break for a part """ @@ -2147,6 +2226,8 @@ class PartSalePriceBreakCreate(AjaxCreateView): model = PartSellPriceBreak form_class = part_forms.EditPartSalePriceBreakForm ajax_form_title = _('Add Price Break') + + role_required = 'part.add' def get_data(self): return { @@ -2197,6 +2278,8 @@ class PartSalePriceBreakEdit(AjaxUpdateView): form_class = part_forms.EditPartSalePriceBreakForm ajax_form_title = _('Edit Price Break') + role_required = 'part.change' + def get_form(self): form = super().get_form() @@ -2211,3 +2294,5 @@ class PartSalePriceBreakDelete(AjaxDeleteView): model = PartSellPriceBreak ajax_form_title = _("Delete Price Break") ajax_template_name = "modal_delete_form.html" + + role_required = 'part.delete' diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 55bc62a44e..ba802b75d9 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -52,6 +52,10 @@ class StockCategoryTree(TreeSerializer): def get_items(self): return StockLocation.objects.all().prefetch_related('stock_items', 'children') + permission_classes = [ + permissions.IsAuthenticated, + ] + class StockDetail(generics.RetrieveUpdateDestroyAPIView): """ API detail endpoint for Stock object @@ -68,7 +72,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): queryset = StockItem.objects.all() serializer_class = StockItemSerializer - permission_classes = (permissions.IsAuthenticated,) def get_queryset(self, *args, **kwargs): @@ -289,10 +292,6 @@ class StockLocationList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -695,10 +694,6 @@ class StockList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -744,10 +739,6 @@ class StockItemTestResultList(generics.ListCreateAPIView): queryset = StockItemTestResult.objects.all() serializer_class = StockItemTestResultSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -799,7 +790,6 @@ class StockTrackingList(generics.ListCreateAPIView): queryset = StockItemTracking.objects.all() serializer_class = StockTrackingSerializer - permission_classes = [permissions.IsAuthenticated] def get_serializer(self, *args, **kwargs): try: @@ -871,7 +861,6 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView): queryset = StockLocation.objects.all() serializer_class = LocationSerializer - permission_classes = (permissions.IsAuthenticated,) stock_endpoints = [ diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index b3fb9af743..928aa6b7a1 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -65,7 +65,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% else %} {{ item.part.full_name }} × {% decimal item.quantity %} {% endif %} -{% if user.is_staff and perms.stock.change_stockitem %} +{% if user.is_staff and roles.stock.change %} {% endif %} diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 2f319f4925..d411891078 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -8,7 +8,7 @@ {% if location %}

    {{ location.name }} - {% if user.is_staff and perms.stock.change_stocklocation %} + {% if user.is_staff and roles.stock.change %} {% endif %}

    diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index a522bc5415..8348a3e331 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -3,6 +3,8 @@ from rest_framework import status from django.urls import reverse from django.contrib.auth import get_user_model +from InvenTree.helpers import addUserPermissions + from .models import StockLocation @@ -22,6 +24,20 @@ class StockAPITestCase(APITestCase): # Create a user for auth User = get_user_model() self.user = User.objects.create_user('testuser', 'test@testing.com', 'password') + + # Add the necessary permissions to the user + perms = [ + 'view_stockitemtestresult', + 'change_stockitemtestresult', + 'add_stockitemtestresult', + 'add_stocklocation', + 'change_stocklocation', + 'add_stockitem', + 'change_stockitem', + ] + + addUserPermissions(self.user, perms) + self.client.login(username='testuser', password='password') def doPost(self, url, data={}): diff --git a/InvenTree/templates/403.html b/InvenTree/templates/403.html new file mode 100644 index 0000000000..372bd9fe27 --- /dev/null +++ b/InvenTree/templates/403.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block page_title %} +InvenTree | {% trans "Permission Denied" %} +{% endblock %} + +{% block content %} + +
    +

    {% trans "Permission Denied" %}

    + +
    + {% trans "You do not have permission to view this page." %} +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index a9e4ae92ea..8e59d51d2b 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -1,7 +1,7 @@ {% extends "base.html" %} - +{% load i18n %} {% block page_title %} -InvenTree | Index +InvenTree | {% trans "Index" %} {% endblock %} {% block content %} @@ -9,24 +9,24 @@ InvenTree | Index
    - {% if perms.part.view_part %} + {% if roles.part.view %} {% include "InvenTree/latest_parts.html" with collapse_id="latest_parts" %} {% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %} {% include "InvenTree/starred_parts.html" with collapse_id="starred" %} {% endif %} - {% if perms.build.view_build %} + {% if roles.build.view %} {% include "InvenTree/build_pending.html" with collapse_id="build_pending" %} {% endif %}
    - {% if perms.stock.view_stockitem %} + {% if roles.stock.view %} {% include "InvenTree/low_stock.html" with collapse_id="order" %} {% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %} {% endif %} - {% if perms.order.view_purchaseorder %} + {% if roles.purchase_order.view %} {% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %} {% endif %} - {% if perms.order.view_salesorder %} + {% if roles.sales_order.view %} {% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %} {% endif %}
    diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 2224d85bc4..148a96c583 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -15,16 +15,16 @@
    diff --git a/InvenTree/users/migrations/0003_auto_20201005_2227.py b/InvenTree/users/migrations/0003_auto_20201005_2227.py new file mode 100644 index 0000000000..92d7e341fa --- /dev/null +++ b/InvenTree/users/migrations/0003_auto_20201005_2227.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.7 on 2020-10-05 22:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_auto_20201004_0158'), + ] + + operations = [ + migrations.AlterField( + model_name='ruleset', + name='can_add', + field=models.BooleanField(default=False, help_text='Permission to add items', verbose_name='Add'), + ), + migrations.AlterField( + model_name='ruleset', + name='can_change', + field=models.BooleanField(default=False, help_text='Permissions to edit items', verbose_name='Change'), + ), + ] diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index df8e91d293..fcf137bb01 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -134,9 +134,9 @@ class RuleSet(models.Model): can_view = models.BooleanField(verbose_name=_('View'), default=True, help_text=_('Permission to view items')) - can_add = models.BooleanField(verbose_name=_('Create'), default=False, help_text=_('Permission to add items')) + can_add = models.BooleanField(verbose_name=_('Add'), default=False, help_text=_('Permission to add items')) - can_change = models.BooleanField(verbose_name=_('Update'), default=False, help_text=_('Permissions to edit items')) + can_change = models.BooleanField(verbose_name=_('Change'), default=False, help_text=_('Permissions to edit items')) can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items')) @@ -178,6 +178,10 @@ class RuleSet(models.Model): super().save(*args, **kwargs) + if self.group: + # Update the group too! + self.group.save() + def get_models(self): """ Return the database tables / models that this ruleset covers. @@ -334,9 +338,37 @@ def create_missing_rule_sets(sender, instance, **kwargs): then we can now use these RuleSet values to update the group permissions. """ - created = kwargs.get('created', False) - # To trigger the group permissions update: update_fields should not be None - update_fields = kwargs.get('update_fields', None) - if created or update_fields: - update_group_roles(instance) + update_group_roles(instance) + + +def check_user_role(user, role, permission): + """ + Check if a user has a particular role:permission combination. + + If the user is a superuser, this will return True + """ + + if user.is_superuser: + return True + + for group in user.groups.all(): + + for rule in group.rule_sets.all(): + + if rule.name == role: + + if permission == 'add' and rule.can_add: + return True + + if permission == 'change' and rule.can_change: + return True + + if permission == 'view' and rule.can_view: + return True + + if permission == 'delete' and rule.can_delete: + return True + + # No matching permissions found + return False