Merge pull request #1024 from SchrodingersGat/part-permissions

Permissions
This commit is contained in:
Oliver 2020-10-06 18:29:05 +11:00 committed by GitHub
commit 606c62078f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1187 additions and 642 deletions

View File

@ -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 = {

View File

@ -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}

View File

@ -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)

View File

@ -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'
}

View File

@ -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)

View File

@ -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,
]

View File

@ -35,7 +35,7 @@ src="{% static 'img/blank_image.png' %}"
<hr>
<h4>
{{ build.quantity }} x {{ build.part.full_name }}
{% if user.is_staff and perms.build.change_build %}
{% if user.is_staff and roles.build.change %}
<a href="{% url 'admin:build_build_change' build.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %}
</h4>

View File

@ -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,
]

View File

@ -23,7 +23,7 @@ InvenTree | {% trans "Company" %} - {{ company.name }}
<hr>
<h4>
{{ company.name }}
{% if user.is_staff and perms.company.change_company %}
{% if user.is_staff and roles.company.change %}
<a href="{% url 'admin:company_company_change' company.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %}
</h4>

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

View File

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

View File

@ -24,7 +24,7 @@ src="{% static 'img/blank_image.png' %}"
<hr>
<h4>
{{ order }}
{% if user.is_staff and perms.order.change_purchaseorder %}
{% if user.is_staff and roles.purchase_order.change %}
<a href="{% url 'admin:order_purchaseorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
{% endif %}
</h4>

View File

@ -34,7 +34,7 @@ src="{% static 'img/blank_image.png' %}"
<hr>
<h4>
{{ order }}
{% if user.is_staff and perms.order.change_salesorder %}
{% if user.is_staff and roles.sales_order.change %}
<a href="{% url 'admin:order_salesorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
{% endif %}
</h4>

View File

@ -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 """

View File

@ -39,10 +39,12 @@
<button class='btn btn-default action-button' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new'><span class='fas fa-plus-circle'></span></button>
<button class='btn btn-default action-button' type='button' title='{% trans "Finish Editing" %}' id='editing-finished'><span class='fas fa-check-circle'></span></button>
{% elif part.active %}
{% if roles.part.change %}
<button class='btn btn-default action-button' type='button' title='{% trans "Edit BOM" %}' id='edit-bom'><span class='fas fa-edit'></span></button>
{% if part.is_bom_valid == False %}
<button class='btn btn-default action-button' id='validate-bom' title='{% trans "Validate Bill of Materials" %}' type='button'><span class='fas fa-clipboard-check'></span></button>
{% endif %}
{% endif %}
<button title='{% trans "Export Bill of Materials" %}' class='btn btn-default action-button' id='download-bom' type='button'><span class='fas fa-file-download'></span></button>
{% endif %}
</div>

View File

@ -1,15 +1,18 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include 'part/tabs.html' with tab='build' %}
<h3>Part Builds</h3>
<h3>{% trans "Part Builds" %}</h3>
<div id='button-toolbar'>
<div class='button-toolbar container-flui' style='float: right';>
{% if part.active %}
<button class="btn btn-success" id='start-build'>Start New Build</button>
{% if roles.build.add %}
<button class="btn btn-success" id='start-build'>{% trans "Start New Build" %}</button>
{% endif %}
{% endif %}
<div class='filter-list' id='filter-list-build'>
<!-- Empty div for filters -->

View File

@ -9,7 +9,7 @@
{% if category %}
<h3>
{{ category.name }}
{% if user.is_staff and perms.part.change_partcategory %}
{% if user.is_staff and roles.part.change %}
<a href="{% url 'admin:part_partcategory_change' category.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %}
</h3>
@ -20,17 +20,23 @@
{% endif %}
<p>
<div class='btn-group action-buttons'>
{% if roles.part.add %}
<button class='btn btn-default' id='cat-create' title='{% trans "Create new part category" %}'>
<span class='fas fa-plus-circle icon-green'/>
</button>
{% endif %}
{% if category %}
{% if roles.part.change %}
<button class='btn btn-default' id='cat-edit' title='{% trans "Edit part category" %}'>
<span class='fas fa-edit icon-blue'/>
</button>
{% endif %}
{% if roles.part.delete %}
<button class='btn btn-default' id='cat-delete' title='{% trans "Delete part category" %}'>
<span class='fas fa-trash-alt icon-red'/>
</button>
{% endif %}
{% endif %}
</div>
</p>
</div>
@ -104,11 +110,15 @@
<div class='button-toolbar container-fluid' style="float: right;">
<div class='btn-group'>
<button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>{% trans "Export" %}</button>
{% if roles.part.add %}
<button class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>{% trans "New Part" %}</button>
{% endif %}
<div class='btn-group'>
<button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown">{% trans "Options" %}<span class='caret'></span></button>
<ul class='dropdown-menu'>
{% if roles.part.change %}
<li><a href='#' id='multi-part-category' title='{% trans "Set category" %}'>{% trans "Set Category" %}</a></li>
{% endif %}
<li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
<li><a href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li>
</ul>
@ -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 () {

View File

@ -29,7 +29,9 @@
<h4>{% trans "Part Notes" %}</h4>
</div>
<div class='col-sm-6'>
{% if roles.part.change %}
<button title='{% trans "Edit notes" %}' class='btn btn-default action-button float-right' id='edit-notes'><span class='fas fa-edit'></span></button>
{% endif %}
</div>
</div>
<hr>

View File

@ -10,7 +10,9 @@
<div id='button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
{% if roles.part.add %}
<button title='{% trans "Add new parameter" %}' class='btn btn-success' id='param-create'>{% trans "New Parameter" %}</button>
{% endif %}
</div>
</div>
@ -30,8 +32,12 @@
<td>
{{ param.template.units }}
<div class='btn-group' style='float: right;'>
{% if roles.part.change %}
<button title='{% trans "Edit" %}' class='btn btn-default btn-glyph param-edit' url="{% url 'part-param-edit' param.id %}" type='button'><span class='fas fa-edit'/></button>
{% endif %}
{% if roles.part.delete %}
<button title='{% trans "Delete" %}' class='btn btn-default btn-glyph param-delete' url="{% url 'part-param-delete' param.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
{% endif %}
</div>
</td>
</tr>
@ -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);

View File

@ -28,7 +28,7 @@
<div class="media-body">
<h3>
{{ part.full_name }}
{% if user.is_staff and perms.part.change_part %}
{% if user.is_staff and roles.part.change %}
<a href="{% url 'admin:part_part_change' part.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %}
{% if not part.active %}
@ -56,26 +56,36 @@
<button type='button' class='btn btn-default' id='price-button' title='{% trans "Show pricing information" %}'>
<span id='part-price-icon' class='fas fa-dollar-sign'/>
</button>
<button type='button' class='btn btn-default' id='part-count' title='Count part stock'>
{% if roles.stock.change %}
<button type='button' class='btn btn-default' id='part-count' title='{% trans "Count part stock" %}'>
<span class='fas fa-clipboard-list'/>
</button>
{% endif %}
{% if part.purchaseable %}
<button type='button' class='btn btn-default' id='part-order' title='Order part'>
{% if roles.purchase_order.add %}
<button type='button' class='btn btn-default' id='part-order' title='{% trans "Order part" %}'>
<span id='part-order-icon' class='fas fa-shopping-cart'/>
</button>
{% endif %}
{% endif %}
{% endif %}
<!-- Part actions -->
{% if roles.part.add or roles.part.change or roles.part.delete %}
<div class='btn-group'>
<button id='part-actions' title='{% trans "Part actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'> <span class='fas fa-shapes'></span> <span class='caret'></span></button>
<ul class='dropdown-menu'>
{% if roles.part.add %}
<li><a href='#' id='part-duplicate'><span class='fas fa-copy'></span> {% trans "Duplicate part" %}</a></li>
{% endif %}
{% if roles.part.change %}
<li><a href='#' id='part-edit'><span class='fas fa-edit icon-blue'></span> {% trans "Edit part" %}</a></li>
{% if not part.active %}
{% endif %}
{% if not part.active and roles.part.delete %}
<li><a href='#' id='part-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete part" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
</div>
<table class='table table-condensed'>
<col width='25'>
@ -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 %}",

View File

@ -26,14 +26,17 @@
{% if part.assembly %}
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}>
<a href="{% url 'part-bom' part.id %}">{% trans "BOM" %}<span class="badge{% if part.is_bom_valid == False %} badge-alert{% endif %}">{{ part.bom_count }}</span></a></li>
{% if roles.build.view %}
<li{% ifequal tab 'build' %} class="active"{% endifequal %}>
<a href="{% url 'part-build' part.id %}">{% trans "Build Orders" %}<span class='badge'>{{ part.builds.count }}</span></a></li>
<a href="{% url 'part-build' part.id %}">{% trans "Build Orders" %}<span class='badge'>{{ part.builds.count }}</span></a>
</li>
{% endif %}
{% endif %}
{% if part.component or part.used_in_count > 0 %}
<li{% ifequal tab 'used' %} class="active"{% endifequal %}>
<a href="{% url 'part-used-in' part.id %}">{% trans "Used In" %} {% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
{% endif %}
{% if part.purchaseable %}
{% if part.purchaseable and roles.purchase_order.view %}
{% if part.is_template == False %}
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
<a href="{% url 'part-suppliers' part.id %}">{% trans "Suppliers" %}
@ -45,7 +48,7 @@
<a href="{% url 'part-orders' part.id %}">{% trans "Purchase Orders" %} <span class='badge'>{{ part.purchase_orders|length }}</span></a>
</li>
{% endif %}
{% if part.salable %}
{% if part.salable and roles.sales_order.view %}
<li {% if tab == 'sales-prices' %}class='active'{% endif %}>
<a href="{% url 'part-sale-prices' part.id %}">{% trans "Sale Price" %}</a>
</li>

View File

@ -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')

View File

@ -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 """

View File

@ -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'

View File

@ -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 = [

View File

@ -65,7 +65,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% else %}
<a href='{% url "part-detail" item.part.pk %}'>{{ item.part.full_name }}</a> &times {% decimal item.quantity %}
{% endif %}
{% if user.is_staff and perms.stock.change_stockitem %}
{% if user.is_staff and roles.stock.change %}
<a href="{% url 'admin:stock_stockitem_change' item.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %}
</h4>

View File

@ -8,7 +8,7 @@
{% if location %}
<h3>
{{ location.name }}
{% if user.is_staff and perms.stock.change_stocklocation %}
{% if user.is_staff and roles.stock.change %}
<a href="{% url 'admin:stock_stocklocation_change' location.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %}
</h3>

View File

@ -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={}):

View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% load i18n %}
{% block page_title %}
InvenTree | {% trans "Permission Denied" %}
{% endblock %}
{% block content %}
<div class='container-fluid'>
<h3>{% trans "Permission Denied" %}</h3>
<div class='alert alert-danger alert-block'>
{% trans "You do not have permission to view this page." %}
</div>
</div>
{% endblock %}

View File

@ -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
<hr>
<div class='col-sm-6'>
{% 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 %}
</div>
<div class='col-sm-6'>
{% 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 %}
</div>

View File

@ -15,16 +15,16 @@
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
{% if perms.part.view_part or perms.part.view_partcategory %}
{% if roles.part.view %}
<li><a href="{% url 'part-index' %}"><span class='fas fa-shapes icon-header'></span>{% trans "Parts" %}</a></li>
{% endif %}
{% if perms.stock.view_stockitem or perms.part.view_stocklocation %}
{% if roles.stock.view %}
<li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li>
{% endif %}
{% if perms.build.view_build %}
{% if roles.build.view %}
<li><a href="{% url 'build-index' %}"><span class='fas fa-tools icon-header'></span>{% trans "Build" %}</a></li>
{% endif %}
{% if perms.order.view_purchaseorder %}
{% if roles.purchase_order.view %}
<li class='nav navbar-nav'>
<a class='dropdown-toggle' data-toggle='dropdown' href='#'><span class='fas fa-shopping-cart icon-header'></span>{% trans "Buy" %}</a>
<ul class='dropdown-menu'>
@ -34,7 +34,7 @@
</ul>
</li>
{% endif %}
{% if perms.order.view_salesorder %}
{% if roles.sales_order.view %}
<li class='nav navbar-nav'>
<a class='dropdown-toggle' data-toggle='dropdown' href='#'><span class='fas fa-truck icon-header'></span>{% trans "Sell" %}</a>
<ul class='dropdown-menu'>

View File

@ -1,3 +1,3 @@
<div>
<input fieldname='{{ field }}' class='slidey' type="checkbox" data-offstyle='warning' data-onstyle="success" data-size='small' data-toggle="toggle" {% if disabled %}disabled {% endif %}{% if state %}checked=""{% endif %} autocomplete="off">
<input fieldname='{{ field }}' class='slidey' type="checkbox" data-offstyle='warning' data-onstyle="success" data-size='small' data-toggle="toggle" {% if disabled or not roles.part.change %}disabled {% endif %}{% if state %}checked=""{% endif %} autocomplete="off">
</div>

View File

@ -6,19 +6,27 @@
<button class='btn btn-default' id='stock-export' title='{% trans "Export Stock Information" %}'>{% trans "Export" %}</button>
{% if read_only %}
{% else %}
{% if roles.stock.add %}
<button class="btn btn-success" id='item-create'>{% trans "New Stock Item" %}</button>
{% endif %}
{% if roles.stock.change or roles.stock.delete %}
<div class="btn-group">
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
<ul class="dropdown-menu">
{% if roles.stock.change %}
<li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'>{% trans "Add stock" %}</a></li>
<li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'>{% trans "Remove stock" %}</a></li>
<li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'>{% trans "Count stock" %}</a></li>
<li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'>{% trans "Move stock" %}</a></li>
<li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'>{% trans "Order stock" %}</a></li>
{% endif %}
{% if roles.stock.delete %}
<li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'>{% trans "Delete Stock" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
</div>
<div class='filter-list' id='filter-list-stock'>
<!-- An empty div in which the filter list will be constructed -->

View File

@ -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'),
),
]

View File

@ -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