mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of git://github.com/inventree/InvenTree into user_unique_group_validation
This commit is contained in:
commit
19a2326638
@ -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 = {
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
}
|
||||
|
||||
|
@ -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, RuleSet
|
||||
|
||||
from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm
|
||||
from .helpers import str2bool
|
||||
@ -107,31 +108,72 @@ 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('.')
|
||||
|
||||
if role not in RuleSet.RULESET_NAMES:
|
||||
raise ValueError(f"Role '{role}' is not a valid role")
|
||||
|
||||
if permission not in RuleSet.RULESET_PERMISSIONS:
|
||||
raise ValueError(f"Permission '{permission}' is not a valid permission")
|
||||
|
||||
# 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)
|
||||
|
@ -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,
|
||||
]
|
||||
|
@ -35,25 +35,27 @@ 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>
|
||||
<div class='btn-row'>
|
||||
<div class='btn-group action-buttons'>
|
||||
<button type='button' class='btn btn-default' id='build-edit' title='Edit Build'>
|
||||
{% if roles.build.change %}
|
||||
<button type='button' class='btn btn-default' id='build-edit' title='{% trans "Edit Build" %}'>
|
||||
<span class='fas fa-edit icon-green'/>
|
||||
</button>
|
||||
{% if build.is_active %}
|
||||
<button type='button' class='btn btn-default' id='build-complete' title="Complete Build">
|
||||
<button type='button' class='btn btn-default' id='build-complete' title='{% trans "Complete Build" %}'>
|
||||
<span class='fas fa-tools'/>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default btn-glyph' id='build-cancel' title='Cancel Build'>
|
||||
<button type='button' class='btn btn-default btn-glyph' id='build-cancel' title='{% trans "Cancel Build" %}'>
|
||||
<span class='fas fa-times-circle icon-red'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if build.status == BuildStatus.CANCELLED %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='build-delete' title='Delete Build'>
|
||||
{% endif %}
|
||||
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='build-delete' title='{% trans "Delete Build" %}'>
|
||||
<span class='fas fa-trash-alt icon-red'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||
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 rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
@ -30,6 +31,20 @@ class BuildTestSimple(TestCase):
|
||||
User.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
|
||||
self.user = User.objects.get(username='testuser')
|
||||
|
||||
g = Group.objects.create(name='builders')
|
||||
self.user.groups.add(g)
|
||||
|
||||
for rule in g.rule_sets.all():
|
||||
if rule.name == 'build':
|
||||
rule.can_change = True
|
||||
rule.can_add = True
|
||||
rule.can_delete = True
|
||||
|
||||
rule.save()
|
||||
|
||||
g.save()
|
||||
|
||||
self.client.login(username='testuser', password='password')
|
||||
|
||||
def test_build_objects(self):
|
||||
@ -94,7 +109,20 @@ class TestBuildAPI(APITestCase):
|
||||
def setUp(self):
|
||||
# Create a user for auth
|
||||
User = get_user_model()
|
||||
User.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
user = User.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
|
||||
g = Group.objects.create(name='builders')
|
||||
user.groups.add(g)
|
||||
|
||||
for rule in g.rule_sets.all():
|
||||
if rule.name == 'build':
|
||||
rule.can_change = True
|
||||
rule.can_add = True
|
||||
rule.can_delete = True
|
||||
|
||||
rule.save()
|
||||
|
||||
g.save()
|
||||
|
||||
self.client.login(username='testuser', password='password')
|
||||
|
||||
@ -131,7 +159,20 @@ class TestBuildViews(TestCase):
|
||||
|
||||
# Create a user
|
||||
User = get_user_model()
|
||||
User.objects.create_user('username', 'user@email.com', 'password')
|
||||
user = User.objects.create_user('username', 'user@email.com', 'password')
|
||||
|
||||
g = Group.objects.create(name='builders')
|
||||
user.groups.add(g)
|
||||
|
||||
for rule in g.rule_sets.all():
|
||||
if rule.name == 'build':
|
||||
rule.can_change = True
|
||||
rule.can_add = True
|
||||
rule.can_delete = True
|
||||
|
||||
rule.save()
|
||||
|
||||
g.save()
|
||||
|
||||
self.client.login(username='username', password='password')
|
||||
|
||||
|
@ -17,16 +17,18 @@ from . import forms
|
||||
from stock.models import StockLocation, StockItem
|
||||
|
||||
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
|
||||
from InvenTree.views import InvenTreeRoleMixin
|
||||
from InvenTree.helpers import str2bool, ExtractSerialNumbers
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
|
||||
|
||||
class BuildIndex(ListView):
|
||||
class BuildIndex(InvenTreeRoleMixin, ListView):
|
||||
""" View for displaying list of Builds
|
||||
"""
|
||||
model = Build
|
||||
template_name = 'build/index.html'
|
||||
context_object_name = 'builds'
|
||||
role_required = 'build.view'
|
||||
|
||||
def get_queryset(self):
|
||||
""" Return all Build objects (order by date, newest first) """
|
||||
@ -56,6 +58,7 @@ class BuildCancel(AjaxUpdateView):
|
||||
ajax_form_title = _('Cancel Build')
|
||||
context_object_name = 'build'
|
||||
form_class = forms.CancelBuildForm
|
||||
role_required = 'build.change'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Handle POST request. Mark the build status as CANCELLED """
|
||||
@ -94,6 +97,7 @@ class BuildAutoAllocate(AjaxUpdateView):
|
||||
context_object_name = 'build'
|
||||
ajax_form_title = _('Allocate Stock')
|
||||
ajax_template_name = 'build/auto_allocate.html'
|
||||
role_required = 'build.change'
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
""" Get the context data for form rendering. """
|
||||
@ -147,6 +151,7 @@ class BuildUnallocate(AjaxUpdateView):
|
||||
form_class = forms.ConfirmBuildForm
|
||||
ajax_form_title = _("Unallocate Stock")
|
||||
ajax_template_name = "build/unallocate.html"
|
||||
form_required = 'build.change'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
@ -184,6 +189,7 @@ class BuildComplete(AjaxUpdateView):
|
||||
context_object_name = "build"
|
||||
ajax_form_title = _("Complete Build")
|
||||
ajax_template_name = "build/complete.html"
|
||||
role_required = 'build.change'
|
||||
|
||||
def get_form(self):
|
||||
""" Get the form object.
|
||||
@ -325,6 +331,7 @@ class BuildNotes(UpdateView):
|
||||
context_object_name = 'build'
|
||||
template_name = 'build/notes.html'
|
||||
model = Build
|
||||
role_required = 'build.view'
|
||||
|
||||
fields = ['notes']
|
||||
|
||||
@ -342,9 +349,11 @@ class BuildNotes(UpdateView):
|
||||
|
||||
class BuildDetail(DetailView):
|
||||
""" Detail view of a single Build object. """
|
||||
|
||||
model = Build
|
||||
template_name = 'build/detail.html'
|
||||
context_object_name = 'build'
|
||||
role_required = 'build.view'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@ -363,6 +372,7 @@ class BuildAllocate(DetailView):
|
||||
model = Build
|
||||
context_object_name = 'build'
|
||||
template_name = 'build/allocate.html'
|
||||
role_required = ['build.change']
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Provide extra context information for the Build allocation page """
|
||||
@ -392,6 +402,7 @@ class BuildCreate(AjaxCreateView):
|
||||
form_class = forms.EditBuildForm
|
||||
ajax_form_title = _('Start new Build')
|
||||
ajax_template_name = 'modal_form.html'
|
||||
role_required = 'build.add'
|
||||
|
||||
def get_initial(self):
|
||||
""" Get initial parameters for Build creation.
|
||||
@ -427,6 +438,7 @@ class BuildUpdate(AjaxUpdateView):
|
||||
context_object_name = 'build'
|
||||
ajax_form_title = _('Edit Build Details')
|
||||
ajax_template_name = 'modal_form.html'
|
||||
role_required = 'build.change'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -440,6 +452,7 @@ class BuildDelete(AjaxDeleteView):
|
||||
model = Build
|
||||
ajax_template_name = 'build/delete_build.html'
|
||||
ajax_form_title = _('Delete Build')
|
||||
role_required = 'build.delete'
|
||||
|
||||
|
||||
class BuildItemDelete(AjaxDeleteView):
|
||||
@ -451,6 +464,7 @@ class BuildItemDelete(AjaxDeleteView):
|
||||
ajax_template_name = 'build/delete_build_item.html'
|
||||
ajax_form_title = _('Unallocate Stock')
|
||||
context_object_name = 'item'
|
||||
role_required = 'build.delete'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -465,6 +479,7 @@ class BuildItemCreate(AjaxCreateView):
|
||||
form_class = forms.EditBuildItemForm
|
||||
ajax_template_name = 'build/create_build_item.html'
|
||||
ajax_form_title = _('Allocate new Part')
|
||||
role_required = 'build.add'
|
||||
|
||||
part = None
|
||||
available_stock = None
|
||||
@ -618,6 +633,7 @@ class BuildItemEdit(AjaxUpdateView):
|
||||
ajax_template_name = 'modal_form.html'
|
||||
form_class = forms.EditBuildItemForm
|
||||
ajax_form_title = _('Edit Stock Allocation')
|
||||
role_required = 'build.change'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
|
@ -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,
|
||||
]
|
||||
|
@ -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>
|
||||
|
@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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):
|
||||
"""
|
||||
|
@ -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>
|
||||
@ -32,29 +32,31 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<p>
|
||||
<div class='btn-row'>
|
||||
<div class='btn-group action-buttons'>
|
||||
<button type='button' class='btn btn-default' id='edit-order' title='Edit order information'>
|
||||
{% if roles.purchase_order.change %}
|
||||
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
||||
<span class='fas fa-edit icon-green'></span>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default' id='export-order' title='Export order to file'>
|
||||
<span class='fas fa-file-download'></span>
|
||||
</button>
|
||||
{% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %}
|
||||
<button type='button' class='btn btn-default' id='place-order' title='Place order'>
|
||||
<button type='button' class='btn btn-default' id='place-order' title='{% trans "Place order" %}'>
|
||||
<span class='fas fa-paper-plane icon-blue'></span>
|
||||
</button>
|
||||
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
||||
<button type='button' class='btn btn-default' id='receive-order' title='Receive items'>
|
||||
<button type='button' class='btn btn-default' id='receive-order' title='{% trans "Receive items" %}'>
|
||||
<span class='fas fa-clipboard-check'></span>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default' id='complete-order' title='Mark order as complete'>
|
||||
<button type='button' class='btn btn-default' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
||||
<span class='fas fa-check-circle'></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %}
|
||||
<button type='button' class='btn btn-default' id='cancel-order' title='Cancel order'>
|
||||
<button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'>
|
||||
<span class='fas fa-times-circle icon-red'></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
|
||||
<span class='fas fa-file-download'></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
|
@ -28,9 +28,11 @@
|
||||
<div class='col-sm-6'>
|
||||
<h4>{% trans "Order Notes" %}</h4>
|
||||
</div>
|
||||
{% if roles.purchase_order.change %}
|
||||
<div class='col-sm-6'>
|
||||
<button title='{% trans "Edit notes" %}' class='btn btn-default action-button float-right' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
<div class='panel panel-default'>
|
||||
|
@ -14,7 +14,6 @@
|
||||
|
||||
{% include "attachment_table.html" with attachments=order.attachments.all %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
|
@ -12,7 +12,7 @@
|
||||
<hr>
|
||||
|
||||
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
|
||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
|
||||
<button type='button' class='btn btn-default' id='new-po-line'>{% trans "Add Line Item" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -209,12 +209,12 @@ $("#po-table").inventreeTable({
|
||||
|
||||
var pk = row.pk;
|
||||
|
||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.delete %}
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
|
||||
{% endif %}
|
||||
|
||||
{% if order.status == PurchaseOrderStatus.PLACED %}
|
||||
{% if order.status == PurchaseOrderStatus.PLACED and roles.purchase_order.change %}
|
||||
if (row.received < row.quantity) {
|
||||
html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}');
|
||||
}
|
||||
|
@ -14,7 +14,9 @@ InvenTree | {% trans "Purchase Orders" %}
|
||||
|
||||
<div id='table-buttons'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
{% if roles.purchase_order.add %}
|
||||
<button class='btn btn-primary' type='button' id='po-create' title='{% trans "Create new purchase order" %}'>{% trans "New Purchase Order" %}</button>
|
||||
{% endif %}
|
||||
<div class='filter-list' id='filter-list-purchaseorder'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
|
@ -34,19 +34,17 @@ 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>
|
||||
<p>{{ order.description }}</p>
|
||||
<div class='btn-row'>
|
||||
<div class='btn-group action-buttons'>
|
||||
{% if roles.sales_order.change %}
|
||||
<button type='button' class='btn btn-default' id='edit-order' title='Edit order information'>
|
||||
<span class='fas fa-edit icon-green'></span>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default' id='packing-list' title='{% trans "Packing List" %}'>
|
||||
<span class='fas fa-clipboard-list'></span>
|
||||
</button>
|
||||
{% if order.status == SalesOrderStatus.PENDING %}
|
||||
<button type='button' class='btn btn-default' id='ship-order' title='{% trans "Ship order" %}'>
|
||||
<span class='fas fa-paper-plane icon-blue'></span>
|
||||
@ -55,6 +53,10 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<span class='fas fa-times-circle icon-red'></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button type='button' disabled='' class='btn btn-default' id='packing-list' title='{% trans "Packing List" %}'>
|
||||
<span class='fas fa-clipboard-list'></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -14,7 +14,9 @@ InvenTree | {% trans "Sales Orders" %}
|
||||
|
||||
<div id='table-buttons'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
{% if roles.sales_order.add %}
|
||||
<button class='btn btn-primary' type='button' id='so-create' title='{% trans "Create new sales order" %}'>{% trans "New Sales Order" %}</button>
|
||||
{% endif %}
|
||||
<div class='filter-list' id='filter-list-salesorder'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ from __future__ import unicode_literals
|
||||
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 InvenTree.status_codes import PurchaseOrderStatus
|
||||
|
||||
@ -32,7 +33,21 @@ class OrderViewTestCase(TestCase):
|
||||
|
||||
# Create a user
|
||||
User = get_user_model()
|
||||
User.objects.create_user('username', 'user@email.com', 'password')
|
||||
user = User.objects.create_user('username', 'user@email.com', 'password')
|
||||
|
||||
# Ensure that the user has the correct permissions!
|
||||
g = Group.objects.create(name='orders')
|
||||
user.groups.add(g)
|
||||
|
||||
for rule in g.rule_sets.all():
|
||||
if rule.name in ['purchase_order', 'sales_order']:
|
||||
rule.can_change = True
|
||||
rule.can_add = True
|
||||
rule.can_delete = True
|
||||
|
||||
rule.save()
|
||||
|
||||
g.save()
|
||||
|
||||
self.client.login(username='username', password='password')
|
||||
|
||||
|
@ -28,19 +28,22 @@ from . import forms as order_forms
|
||||
|
||||
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||
from InvenTree.helpers import DownloadFile, str2bool
|
||||
from InvenTree.views import InvenTreeRoleMixin
|
||||
|
||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PurchaseOrderIndex(ListView):
|
||||
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
""" List view for all purchase orders """
|
||||
|
||||
model = PurchaseOrder
|
||||
template_name = 'order/purchase_orders.html'
|
||||
context_object_name = 'orders'
|
||||
|
||||
role_required = 'purchase_order.view'
|
||||
|
||||
def get_queryset(self):
|
||||
""" Retrieve the list of purchase orders,
|
||||
ensure that the most recent ones are returned first. """
|
||||
@ -55,19 +58,21 @@ class PurchaseOrderIndex(ListView):
|
||||
return ctx
|
||||
|
||||
|
||||
class SalesOrderIndex(ListView):
|
||||
class SalesOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
|
||||
model = SalesOrder
|
||||
template_name = 'order/sales_orders.html'
|
||||
context_object_name = 'orders'
|
||||
role_required = 'sales_order.view'
|
||||
|
||||
|
||||
class PurchaseOrderDetail(DetailView):
|
||||
class PurchaseOrderDetail(InvenTreeRoleMixin, DetailView):
|
||||
""" Detail view for a PurchaseOrder object """
|
||||
|
||||
context_object_name = 'order'
|
||||
queryset = PurchaseOrder.objects.all().prefetch_related('lines')
|
||||
template_name = 'order/purchase_order_detail.html'
|
||||
role_required = 'purchase_order.view'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
@ -75,12 +80,13 @@ class PurchaseOrderDetail(DetailView):
|
||||
return ctx
|
||||
|
||||
|
||||
class SalesOrderDetail(DetailView):
|
||||
class SalesOrderDetail(InvenTreeRoleMixin, DetailView):
|
||||
""" Detail view for a SalesOrder object """
|
||||
|
||||
context_object_name = 'order'
|
||||
queryset = SalesOrder.objects.all().prefetch_related('lines')
|
||||
template_name = 'order/sales_order_detail.html'
|
||||
role_required = 'sales_order.view'
|
||||
|
||||
|
||||
class PurchaseOrderAttachmentCreate(AjaxCreateView):
|
||||
@ -92,6 +98,7 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView):
|
||||
form_class = order_forms.EditPurchaseOrderAttachmentForm
|
||||
ajax_form_title = _("Add Purchase Order Attachment")
|
||||
ajax_template_name = "modal_form.html"
|
||||
role_required = 'purchase_order.add'
|
||||
|
||||
def post_save(self, **kwargs):
|
||||
self.object.user = self.request.user
|
||||
@ -139,6 +146,7 @@ class SalesOrderAttachmentCreate(AjaxCreateView):
|
||||
model = SalesOrderAttachment
|
||||
form_class = order_forms.EditSalesOrderAttachmentForm
|
||||
ajax_form_title = _('Add Sales Order Attachment')
|
||||
role_required = 'sales_order.add'
|
||||
|
||||
def post_save(self, **kwargs):
|
||||
self.object.user = self.request.user
|
||||
@ -174,6 +182,7 @@ class PurchaseOrderAttachmentEdit(AjaxUpdateView):
|
||||
model = PurchaseOrderAttachment
|
||||
form_class = order_forms.EditPurchaseOrderAttachmentForm
|
||||
ajax_form_title = _("Edit Attachment")
|
||||
role_required = 'purchase_order.change'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -195,6 +204,7 @@ class SalesOrderAttachmentEdit(AjaxUpdateView):
|
||||
model = SalesOrderAttachment
|
||||
form_class = order_forms.EditSalesOrderAttachmentForm
|
||||
ajax_form_title = _("Edit Attachment")
|
||||
role_required = 'sales_order.change'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -216,6 +226,7 @@ class PurchaseOrderAttachmentDelete(AjaxDeleteView):
|
||||
ajax_form_title = _("Delete Attachment")
|
||||
ajax_template_name = "order/delete_attachment.html"
|
||||
context_object_name = "attachment"
|
||||
role_required = 'purchase_order.delete'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -230,6 +241,7 @@ class SalesOrderAttachmentDelete(AjaxDeleteView):
|
||||
ajax_form_title = _("Delete Attachment")
|
||||
ajax_template_name = "order/delete_attachment.html"
|
||||
context_object_name = "attachment"
|
||||
role_required = 'sales_order.delete'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -237,12 +249,13 @@ class SalesOrderAttachmentDelete(AjaxDeleteView):
|
||||
}
|
||||
|
||||
|
||||
class PurchaseOrderNotes(UpdateView):
|
||||
class PurchaseOrderNotes(InvenTreeRoleMixin, UpdateView):
|
||||
""" View for updating the 'notes' field of a PurchaseOrder """
|
||||
|
||||
context_object_name = 'order'
|
||||
template_name = 'order/order_notes.html'
|
||||
model = PurchaseOrder
|
||||
role_required = 'purchase_order.view'
|
||||
|
||||
fields = ['notes']
|
||||
|
||||
@ -259,12 +272,13 @@ class PurchaseOrderNotes(UpdateView):
|
||||
return ctx
|
||||
|
||||
|
||||
class SalesOrderNotes(UpdateView):
|
||||
class SalesOrderNotes(InvenTreeRoleMixin, UpdateView):
|
||||
""" View for editing the 'notes' field of a SalesORder """
|
||||
|
||||
context_object_name = 'order'
|
||||
template_name = 'order/sales_order_notes.html'
|
||||
model = SalesOrder
|
||||
role_required = 'sales_order.view'
|
||||
|
||||
fields = ['notes']
|
||||
|
||||
@ -286,6 +300,7 @@ class PurchaseOrderCreate(AjaxCreateView):
|
||||
model = PurchaseOrder
|
||||
ajax_form_title = _("Create Purchase Order")
|
||||
form_class = order_forms.EditPurchaseOrderForm
|
||||
role_required = 'purchase_order.add'
|
||||
|
||||
def get_initial(self):
|
||||
initials = super().get_initial().copy()
|
||||
@ -317,6 +332,7 @@ class SalesOrderCreate(AjaxCreateView):
|
||||
model = SalesOrder
|
||||
ajax_form_title = _("Create Sales Order")
|
||||
form_class = order_forms.EditSalesOrderForm
|
||||
role_required = 'sales_order.add'
|
||||
|
||||
def get_initial(self):
|
||||
initials = super().get_initial().copy()
|
||||
@ -347,6 +363,7 @@ class PurchaseOrderEdit(AjaxUpdateView):
|
||||
model = PurchaseOrder
|
||||
ajax_form_title = _('Edit Purchase Order')
|
||||
form_class = order_forms.EditPurchaseOrderForm
|
||||
role_required = 'purchase_order.change'
|
||||
|
||||
def get_form(self):
|
||||
|
||||
@ -367,6 +384,7 @@ class SalesOrderEdit(AjaxUpdateView):
|
||||
model = SalesOrder
|
||||
ajax_form_title = _('Edit Sales Order')
|
||||
form_class = order_forms.EditSalesOrderForm
|
||||
role_required = 'sales_order.change'
|
||||
|
||||
def get_form(self):
|
||||
form = super().get_form()
|
||||
@ -384,6 +402,7 @@ class PurchaseOrderCancel(AjaxUpdateView):
|
||||
ajax_form_title = _('Cancel Order')
|
||||
ajax_template_name = 'order/order_cancel.html'
|
||||
form_class = order_forms.CancelPurchaseOrderForm
|
||||
role_required = 'purchase_order.change'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Mark the PO as 'CANCELLED' """
|
||||
@ -417,6 +436,7 @@ class SalesOrderCancel(AjaxUpdateView):
|
||||
ajax_form_title = _("Cancel sales order")
|
||||
ajax_template_name = "order/sales_order_cancel.html"
|
||||
form_class = order_forms.CancelSalesOrderForm
|
||||
role_required = 'sales_order.change'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
@ -451,6 +471,7 @@ class PurchaseOrderIssue(AjaxUpdateView):
|
||||
ajax_form_title = _('Issue Order')
|
||||
ajax_template_name = "order/order_issue.html"
|
||||
form_class = order_forms.IssuePurchaseOrderForm
|
||||
role_required = 'purchase_order.change'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Mark the purchase order as 'PLACED' """
|
||||
@ -486,6 +507,7 @@ class PurchaseOrderComplete(AjaxUpdateView):
|
||||
ajax_template_name = "order/order_complete.html"
|
||||
ajax_form_title = _("Complete Order")
|
||||
context_object_name = 'order'
|
||||
role_required = 'purchase_order.change'
|
||||
|
||||
def get_context_data(self):
|
||||
|
||||
@ -520,6 +542,7 @@ class SalesOrderShip(AjaxUpdateView):
|
||||
context_object_name = 'order'
|
||||
ajax_template_name = 'order/sales_order_ship.html'
|
||||
ajax_form_title = _('Ship Order')
|
||||
role_required = 'sales_order.change'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
@ -563,6 +586,7 @@ class PurchaseOrderExport(AjaxView):
|
||||
"""
|
||||
|
||||
model = PurchaseOrder
|
||||
role_required = 'purchase_order.view'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@ -594,6 +618,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
form_class = order_forms.ReceivePurchaseOrderForm
|
||||
ajax_form_title = _("Receive Parts")
|
||||
ajax_template_name = "order/receive_parts.html"
|
||||
role_required = 'purchase_order.change'
|
||||
|
||||
# Where the parts will be going (selected in POST request)
|
||||
destination = None
|
||||
@ -779,6 +804,11 @@ class OrderParts(AjaxView):
|
||||
ajax_form_title = _("Order Parts")
|
||||
ajax_template_name = 'order/order_wizard/select_parts.html'
|
||||
|
||||
role_required = [
|
||||
'part.view',
|
||||
'purchase_order.change',
|
||||
]
|
||||
|
||||
# List of Parts we wish to order
|
||||
parts = []
|
||||
suppliers = []
|
||||
@ -1085,6 +1115,7 @@ class POLineItemCreate(AjaxCreateView):
|
||||
context_object_name = 'line'
|
||||
form_class = order_forms.EditPurchaseOrderLineItemForm
|
||||
ajax_form_title = _('Add Line Item')
|
||||
role_required = 'purchase_order.add'
|
||||
|
||||
def post(self, request, *arg, **kwargs):
|
||||
|
||||
@ -1199,6 +1230,7 @@ class SOLineItemCreate(AjaxCreateView):
|
||||
context_order_name = 'line'
|
||||
form_class = order_forms.EditSalesOrderLineItemForm
|
||||
ajax_form_title = _('Add Line Item')
|
||||
role_required = 'sales_order.add'
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
|
||||
@ -1250,6 +1282,7 @@ class SOLineItemEdit(AjaxUpdateView):
|
||||
model = SalesOrderLineItem
|
||||
form_class = order_forms.EditSalesOrderLineItemForm
|
||||
ajax_form_title = _('Edit Line Item')
|
||||
role_required = 'sales_order.change'
|
||||
|
||||
def get_form(self):
|
||||
form = super().get_form()
|
||||
@ -1268,6 +1301,7 @@ class POLineItemEdit(AjaxUpdateView):
|
||||
form_class = order_forms.EditPurchaseOrderLineItemForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = _('Edit Line Item')
|
||||
role_required = 'purchase_order.change'
|
||||
|
||||
def get_form(self):
|
||||
form = super().get_form()
|
||||
@ -1285,6 +1319,7 @@ class POLineItemDelete(AjaxDeleteView):
|
||||
model = PurchaseOrderLineItem
|
||||
ajax_form_title = _('Delete Line Item')
|
||||
ajax_template_name = 'order/po_lineitem_delete.html'
|
||||
role_required = 'purchase_order.delete'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -1297,6 +1332,7 @@ class SOLineItemDelete(AjaxDeleteView):
|
||||
model = SalesOrderLineItem
|
||||
ajax_form_title = _("Delete Line Item")
|
||||
ajax_template_name = "order/so_lineitem_delete.html"
|
||||
role_required = 'sales_order.delete'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -1310,6 +1346,7 @@ class SalesOrderAllocationCreate(AjaxCreateView):
|
||||
model = SalesOrderAllocation
|
||||
form_class = order_forms.EditSalesOrderAllocationForm
|
||||
ajax_form_title = _('Allocate Stock to Order')
|
||||
role_required = 'sales_order.add'
|
||||
|
||||
def get_initial(self):
|
||||
initials = super().get_initial().copy()
|
||||
@ -1379,6 +1416,7 @@ class SalesOrderAllocationEdit(AjaxUpdateView):
|
||||
model = SalesOrderAllocation
|
||||
form_class = order_forms.EditSalesOrderAllocationForm
|
||||
ajax_form_title = _('Edit Allocation Quantity')
|
||||
role_required = 'sales_order.change'
|
||||
|
||||
def get_form(self):
|
||||
form = super().get_form()
|
||||
@ -1396,3 +1434,4 @@ class SalesOrderAllocationDelete(AjaxDeleteView):
|
||||
ajax_form_title = _("Remove allocation")
|
||||
context_object_name = 'allocation'
|
||||
ajax_template_name = "order/so_allocation_delete.html"
|
||||
role_required = 'sales_order.delete'
|
||||
|
@ -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 """
|
||||
|
@ -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>
|
||||
|
@ -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 -->
|
||||
|
@ -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 () {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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 %}",
|
||||
|
@ -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>
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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 """
|
||||
|
||||
|
@ -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
|
||||
@ -124,6 +130,8 @@ class PartAttachmentEdit(AjaxUpdateView):
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = _('Edit attachment')
|
||||
|
||||
role_required = 'part.change'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'success': _('Part attachment updated')
|
||||
@ -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')
|
||||
@ -158,6 +168,8 @@ class PartTestTemplateCreate(AjaxCreateView):
|
||||
form_class = part_forms.EditPartTestTemplateForm
|
||||
ajax_form_title = _("Create Test Template")
|
||||
|
||||
role_required = 'part.add'
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initials = super().get_initial()
|
||||
@ -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 """
|
||||
@ -2148,6 +2227,8 @@ class PartSalePriceBreakCreate(AjaxCreateView):
|
||||
form_class = part_forms.EditPartSalePriceBreakForm
|
||||
ajax_form_title = _('Add Price Break')
|
||||
|
||||
role_required = 'part.add'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'success': _('Added new price break')
|
||||
@ -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'
|
||||
|
@ -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 = [
|
||||
|
@ -65,7 +65,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
{% else %}
|
||||
<a href='{% url "part-detail" item.part.pk %}'>{{ item.part.full_name }}</a> × {% 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>
|
||||
|
@ -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>
|
||||
|
@ -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={}):
|
||||
|
18
InvenTree/templates/403.html
Normal file
18
InvenTree/templates/403.html
Normal 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 %}
|
@ -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>
|
||||
|
@ -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'>
|
||||
|
@ -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>
|
@ -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 -->
|
||||
|
@ -119,15 +119,12 @@ class RoleGroupAdmin(admin.ModelAdmin):
|
||||
# Save inlines before model
|
||||
# https://stackoverflow.com/a/14860703/12794913
|
||||
def save_model(self, request, obj, form, change):
|
||||
if obj is not None:
|
||||
# Save model immediately only if in 'Add role' view
|
||||
super().save_model(request, obj, form, change)
|
||||
else:
|
||||
pass # don't actually save the parent instance
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
formset.save() # this will save the children
|
||||
form.instance.save() # form.instance is the parent
|
||||
# update_fields is required to trigger permissions update
|
||||
form.instance.save(update_fields=['name']) # form.instance is the parent
|
||||
|
||||
|
||||
class InvenTreeUserAdmin(UserAdmin):
|
||||
|
23
InvenTree/users/migrations/0003_auto_20201005_2227.py
Normal file
23
InvenTree/users/migrations/0003_auto_20201005_2227.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -36,6 +36,10 @@ class RuleSet(models.Model):
|
||||
choice[0] for choice in RULESET_CHOICES
|
||||
]
|
||||
|
||||
RULESET_PERMISSIONS = [
|
||||
'view', 'add', 'change', 'delete',
|
||||
]
|
||||
|
||||
RULESET_MODELS = {
|
||||
'admin': [
|
||||
'auth_group',
|
||||
@ -134,9 +138,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'))
|
||||
|
||||
@ -155,7 +159,14 @@ class RuleSet(models.Model):
|
||||
model=model
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self, debug=False):
|
||||
""" Ruleset string representation """
|
||||
if debug:
|
||||
# Makes debugging easier
|
||||
return f'{str(self.group).ljust(15)}: {self.name.title().ljust(15)} | ' \
|
||||
f'v: {str(self.can_view).ljust(5)} | a: {str(self.can_add).ljust(5)} | ' \
|
||||
f'c: {str(self.can_change).ljust(5)} | d: {str(self.can_delete).ljust(5)}'
|
||||
else:
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -171,6 +182,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.
|
||||
@ -329,3 +344,35 @@ def create_missing_rule_sets(sender, instance, **kwargs):
|
||||
"""
|
||||
|
||||
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
|
||||
|
@ -137,7 +137,8 @@ class RuleSetModelTest(TestCase):
|
||||
|
||||
rule.save()
|
||||
|
||||
group.save()
|
||||
# update_fields is required to trigger permissions update
|
||||
group.save(update_fields=['name'])
|
||||
|
||||
# There should now be three permissions for each rule set
|
||||
self.assertEqual(group.permissions.count(), 3 * len(permission_set))
|
||||
@ -151,7 +152,8 @@ class RuleSetModelTest(TestCase):
|
||||
|
||||
rule.save()
|
||||
|
||||
group.save()
|
||||
# update_fields is required to trigger permissions update
|
||||
group.save(update_fields=['name'])
|
||||
|
||||
# There should now not be any permissions assigned to this group
|
||||
self.assertEqual(group.permissions.count(), 0)
|
||||
|
Loading…
Reference in New Issue
Block a user