Merge branch 'master' of git://github.com/inventree/InvenTree into user_unique_group_validation

This commit is contained in:
eeintech 2020-10-06 10:04:35 -05:00
commit 19a2326638
49 changed files with 1781 additions and 1032 deletions

View File

@ -30,6 +30,8 @@ class InfoView(AjaxView):
Use to confirm that the server is running, etc. Use to confirm that the server is running, etc.
""" """
permission_classes = [permissions.AllowAny]
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
data = { data = {

View File

@ -17,3 +17,43 @@ def status_codes(request):
'BuildStatus': BuildStatus, 'BuildStatus': BuildStatus,
'StockStatus': StockStatus, '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.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.contrib.auth.models import Permission
import InvenTree.version import InvenTree.version
from .settings import MEDIA_URL, STATIC_URL from .settings import MEDIA_URL, STATIC_URL
@ -441,3 +443,21 @@ def validateFilterString(value):
results[k] = v results[k] = v
return results 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.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'InvenTree.context.status_codes', 'InvenTree.context.status_codes',
'InvenTree.context.user_roles',
], ],
}, },
}, },
@ -231,6 +232,10 @@ REST_FRAMEWORK = {
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.TokenAuthentication',
), ),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.DjangoModelPermissions',
),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' '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 part.models import Part, PartCategory
from stock.models import StockLocation, StockItem from stock.models import StockLocation, StockItem
from common.models import InvenTreeSetting, ColorTheme from common.models import InvenTreeSetting, ColorTheme
from users.models import check_user_role, RuleSet
from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm
from .helpers import str2bool from .helpers import str2bool
@ -107,31 +108,72 @@ class TreeSerializer(views.APIView):
return JsonResponse(response, safe=False) 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. """ AjaxMixin provides basic functionality for rendering a Django form to JSON.
Handles jsonResponse rendering, and adds extra data for the modal forms to process Handles jsonResponse rendering, and adds extra data for the modal forms to process
on the client side. on the client side.
Any view which inherits the AjaxMixin will need 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 # By default, allow *any* role
permission_required = '*' role_required = None
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, point to the modal_form template # By default, point to the modal_form template
# (this can be overridden by a child class) # (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 django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters 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.conf.urls import url, include
@ -28,10 +28,6 @@ class BuildList(generics.ListCreateAPIView):
queryset = Build.objects.all() queryset = Build.objects.all()
serializer_class = BuildSerializer serializer_class = BuildSerializer
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -99,10 +95,6 @@ class BuildDetail(generics.RetrieveUpdateAPIView):
queryset = Build.objects.all() queryset = Build.objects.all()
serializer_class = BuildSerializer serializer_class = BuildSerializer
permission_classes = [
permissions.IsAuthenticated,
]
class BuildItemList(generics.ListCreateAPIView): class BuildItemList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of BuildItem objects """ API endpoint for accessing a list of BuildItem objects
@ -137,10 +129,6 @@ class BuildItemList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
] ]

View File

@ -35,25 +35,27 @@ src="{% static 'img/blank_image.png' %}"
<hr> <hr>
<h4> <h4>
{{ build.quantity }} x {{ build.part.full_name }} {{ 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> <a href="{% url 'admin:build_build_change' build.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %} {% endif %}
</h4> </h4>
<div class='btn-row'> <div class='btn-row'>
<div class='btn-group action-buttons'> <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'/> <span class='fas fa-edit icon-green'/>
</button> </button>
{% if build.is_active %} {% 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'/> <span class='fas fa-tools'/>
</button> </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'/> <span class='fas fa-times-circle icon-red'/>
</button> </button>
{% endif %} {% endif %}
{% if build.status == BuildStatus.CANCELLED %} {% endif %}
<button type='button' class='btn btn-default btn-glyph' id='build-delete' title='Delete Build'> {% 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'/> <span class='fas fa-trash-alt icon-red'/>
</button> </button>
{% endif %} {% endif %}

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from rest_framework import status from rest_framework import status
@ -30,6 +31,20 @@ class BuildTestSimple(TestCase):
User.objects.create_user('testuser', 'test@testing.com', 'password') User.objects.create_user('testuser', 'test@testing.com', 'password')
self.user = User.objects.get(username='testuser') 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') self.client.login(username='testuser', password='password')
def test_build_objects(self): def test_build_objects(self):
@ -94,7 +109,20 @@ class TestBuildAPI(APITestCase):
def setUp(self): def setUp(self):
# Create a user for auth # Create a user for auth
User = get_user_model() 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') self.client.login(username='testuser', password='password')
@ -131,7 +159,20 @@ class TestBuildViews(TestCase):
# Create a user # Create a user
User = get_user_model() 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') self.client.login(username='username', password='password')

View File

@ -17,16 +17,18 @@ from . import forms
from stock.models import StockLocation, StockItem from stock.models import StockLocation, StockItem
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
from InvenTree.views import InvenTreeRoleMixin
from InvenTree.helpers import str2bool, ExtractSerialNumbers from InvenTree.helpers import str2bool, ExtractSerialNumbers
from InvenTree.status_codes import BuildStatus from InvenTree.status_codes import BuildStatus
class BuildIndex(ListView): class BuildIndex(InvenTreeRoleMixin, ListView):
""" View for displaying list of Builds """ View for displaying list of Builds
""" """
model = Build model = Build
template_name = 'build/index.html' template_name = 'build/index.html'
context_object_name = 'builds' context_object_name = 'builds'
role_required = 'build.view'
def get_queryset(self): def get_queryset(self):
""" Return all Build objects (order by date, newest first) """ """ Return all Build objects (order by date, newest first) """
@ -56,6 +58,7 @@ class BuildCancel(AjaxUpdateView):
ajax_form_title = _('Cancel Build') ajax_form_title = _('Cancel Build')
context_object_name = 'build' context_object_name = 'build'
form_class = forms.CancelBuildForm form_class = forms.CancelBuildForm
role_required = 'build.change'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" Handle POST request. Mark the build status as CANCELLED """ """ Handle POST request. Mark the build status as CANCELLED """
@ -94,6 +97,7 @@ class BuildAutoAllocate(AjaxUpdateView):
context_object_name = 'build' context_object_name = 'build'
ajax_form_title = _('Allocate Stock') ajax_form_title = _('Allocate Stock')
ajax_template_name = 'build/auto_allocate.html' ajax_template_name = 'build/auto_allocate.html'
role_required = 'build.change'
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
""" Get the context data for form rendering. """ """ Get the context data for form rendering. """
@ -147,6 +151,7 @@ class BuildUnallocate(AjaxUpdateView):
form_class = forms.ConfirmBuildForm form_class = forms.ConfirmBuildForm
ajax_form_title = _("Unallocate Stock") ajax_form_title = _("Unallocate Stock")
ajax_template_name = "build/unallocate.html" ajax_template_name = "build/unallocate.html"
form_required = 'build.change'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -184,6 +189,7 @@ class BuildComplete(AjaxUpdateView):
context_object_name = "build" context_object_name = "build"
ajax_form_title = _("Complete Build") ajax_form_title = _("Complete Build")
ajax_template_name = "build/complete.html" ajax_template_name = "build/complete.html"
role_required = 'build.change'
def get_form(self): def get_form(self):
""" Get the form object. """ Get the form object.
@ -325,6 +331,7 @@ class BuildNotes(UpdateView):
context_object_name = 'build' context_object_name = 'build'
template_name = 'build/notes.html' template_name = 'build/notes.html'
model = Build model = Build
role_required = 'build.view'
fields = ['notes'] fields = ['notes']
@ -342,9 +349,11 @@ class BuildNotes(UpdateView):
class BuildDetail(DetailView): class BuildDetail(DetailView):
""" Detail view of a single Build object. """ """ Detail view of a single Build object. """
model = Build model = Build
template_name = 'build/detail.html' template_name = 'build/detail.html'
context_object_name = 'build' context_object_name = 'build'
role_required = 'build.view'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -363,6 +372,7 @@ class BuildAllocate(DetailView):
model = Build model = Build
context_object_name = 'build' context_object_name = 'build'
template_name = 'build/allocate.html' template_name = 'build/allocate.html'
role_required = ['build.change']
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Provide extra context information for the Build allocation page """ """ Provide extra context information for the Build allocation page """
@ -392,6 +402,7 @@ class BuildCreate(AjaxCreateView):
form_class = forms.EditBuildForm form_class = forms.EditBuildForm
ajax_form_title = _('Start new Build') ajax_form_title = _('Start new Build')
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
role_required = 'build.add'
def get_initial(self): def get_initial(self):
""" Get initial parameters for Build creation. """ Get initial parameters for Build creation.
@ -427,6 +438,7 @@ class BuildUpdate(AjaxUpdateView):
context_object_name = 'build' context_object_name = 'build'
ajax_form_title = _('Edit Build Details') ajax_form_title = _('Edit Build Details')
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
role_required = 'build.change'
def get_data(self): def get_data(self):
return { return {
@ -440,6 +452,7 @@ class BuildDelete(AjaxDeleteView):
model = Build model = Build
ajax_template_name = 'build/delete_build.html' ajax_template_name = 'build/delete_build.html'
ajax_form_title = _('Delete Build') ajax_form_title = _('Delete Build')
role_required = 'build.delete'
class BuildItemDelete(AjaxDeleteView): class BuildItemDelete(AjaxDeleteView):
@ -451,6 +464,7 @@ class BuildItemDelete(AjaxDeleteView):
ajax_template_name = 'build/delete_build_item.html' ajax_template_name = 'build/delete_build_item.html'
ajax_form_title = _('Unallocate Stock') ajax_form_title = _('Unallocate Stock')
context_object_name = 'item' context_object_name = 'item'
role_required = 'build.delete'
def get_data(self): def get_data(self):
return { return {
@ -465,6 +479,7 @@ class BuildItemCreate(AjaxCreateView):
form_class = forms.EditBuildItemForm form_class = forms.EditBuildItemForm
ajax_template_name = 'build/create_build_item.html' ajax_template_name = 'build/create_build_item.html'
ajax_form_title = _('Allocate new Part') ajax_form_title = _('Allocate new Part')
role_required = 'build.add'
part = None part = None
available_stock = None available_stock = None
@ -618,6 +633,7 @@ class BuildItemEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
form_class = forms.EditBuildItemForm form_class = forms.EditBuildItemForm
ajax_form_title = _('Edit Stock Allocation') ajax_form_title = _('Edit Stock Allocation')
role_required = 'build.change'
def get_data(self): def get_data(self):
return { return {

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters 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.conf.urls import url, include
from django.db.models import Q from django.db.models import Q
@ -40,10 +40,6 @@ class CompanyList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -82,10 +78,6 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated,
]
class SupplierPartList(generics.ListCreateAPIView): class SupplierPartList(generics.ListCreateAPIView):
""" API endpoint for list view of SupplierPart object """ API endpoint for list view of SupplierPart object
@ -170,10 +162,6 @@ class SupplierPartList(generics.ListCreateAPIView):
serializer_class = SupplierPartSerializer serializer_class = SupplierPartSerializer
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -202,7 +190,6 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = SupplierPart.objects.all() queryset = SupplierPart.objects.all()
serializer_class = SupplierPartSerializer serializer_class = SupplierPartSerializer
permission_classes = (permissions.IsAuthenticated,)
read_only_fields = [ read_only_fields = [
] ]
@ -218,10 +205,6 @@ class SupplierPriceBreakList(generics.ListCreateAPIView):
queryset = SupplierPriceBreak.objects.all() queryset = SupplierPriceBreak.objects.all()
serializer_class = SupplierPriceBreakSerializer serializer_class = SupplierPriceBreakSerializer
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
] ]

View File

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

View File

@ -3,6 +3,8 @@ from rest_framework import status
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from InvenTree.helpers import addUserPermissions
from .models import Company from .models import Company
@ -14,7 +16,16 @@ class CompanyTest(APITestCase):
def setUp(self): def setUp(self):
# Create a user for auth # Create a user for auth
User = get_user_model() 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') self.client.login(username='testuser', password='password')
Company.objects.create(name='ACME', description='Supplier', is_customer=False, is_supplier=True) 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

View File

@ -6,7 +6,7 @@ JSON API for the Order app
from __future__ import unicode_literals from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions from rest_framework import generics
from rest_framework import filters from rest_framework import filters
from django.conf.urls import url, include from django.conf.urls import url, include
@ -109,10 +109,6 @@ class POList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -162,10 +158,6 @@ class PODetail(generics.RetrieveUpdateAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated
]
class POLineItemList(generics.ListCreateAPIView): class POLineItemList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of POLineItem objects """ API endpoint for accessing a list of POLineItem objects
@ -188,10 +180,6 @@ class POLineItemList(generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
] ]
@ -208,10 +196,6 @@ class POLineItemDetail(generics.RetrieveUpdateAPIView):
queryset = PurchaseOrderLineItem queryset = PurchaseOrderLineItem
serializer_class = POLineItemSerializer serializer_class = POLineItemSerializer
permission_classes = [
permissions.IsAuthenticated,
]
class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin): class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
@ -300,10 +284,6 @@ class SOList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -351,8 +331,6 @@ class SODetail(generics.RetrieveUpdateAPIView):
return queryset return queryset
permission_classes = [permissions.IsAuthenticated]
class SOLineItemList(generics.ListCreateAPIView): class SOLineItemList(generics.ListCreateAPIView):
""" """
@ -398,8 +376,6 @@ class SOLineItemList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend]
filter_fields = [ filter_fields = [
@ -414,8 +390,6 @@ class SOLineItemDetail(generics.RetrieveUpdateAPIView):
queryset = SalesOrderLineItem.objects.all() queryset = SalesOrderLineItem.objects.all()
serializer_class = SOLineItemSerializer serializer_class = SOLineItemSerializer
permission_classes = [permissions.IsAuthenticated]
class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """

View File

@ -24,7 +24,7 @@ src="{% static 'img/blank_image.png' %}"
<hr> <hr>
<h4> <h4>
{{ order }} {{ 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> <a href="{% url 'admin:order_purchaseorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
{% endif %} {% endif %}
</h4> </h4>
@ -32,29 +32,31 @@ src="{% static 'img/blank_image.png' %}"
<p> <p>
<div class='btn-row'> <div class='btn-row'>
<div class='btn-group action-buttons'> <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> <span class='fas fa-edit icon-green'></span>
</button> </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 %} {% 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> <span class='fas fa-paper-plane icon-blue'></span>
</button> </button>
{% elif order.status == PurchaseOrderStatus.PLACED %} {% 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> <span class='fas fa-clipboard-check'></span>
</button> </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> <span class='fas fa-check-circle'></span>
</button> </button>
{% endif %} {% endif %}
{% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %} {% 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> <span class='fas fa-times-circle icon-red'></span>
</button> </button>
{% endif %} {% 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>
</div> </div>
</p> </p>

View File

@ -28,9 +28,11 @@
<div class='col-sm-6'> <div class='col-sm-6'>
<h4>{% trans "Order Notes" %}</h4> <h4>{% trans "Order Notes" %}</h4>
</div> </div>
{% if roles.purchase_order.change %}
<div class='col-sm-6'> <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> <button title='{% trans "Edit notes" %}' class='btn btn-default action-button float-right' id='edit-notes'><span class='fas fa-edit'></span></button>
</div> </div>
{% endif %}
</div> </div>
<hr> <hr>
<div class='panel panel-default'> <div class='panel panel-default'>

View File

@ -14,7 +14,6 @@
{% include "attachment_table.html" with attachments=order.attachments.all %} {% include "attachment_table.html" with attachments=order.attachments.all %}
{% endblock %} {% endblock %}
{% block js_ready %} {% block js_ready %}

View File

@ -12,7 +12,7 @@
<hr> <hr>
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'> <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> <button type='button' class='btn btn-default' id='new-po-line'>{% trans "Add Line Item" %}</button>
{% endif %} {% endif %}
</div> </div>
@ -209,12 +209,12 @@ $("#po-table").inventreeTable({
var pk = row.pk; 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-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" %}'); html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
{% endif %} {% endif %}
{% if order.status == PurchaseOrderStatus.PLACED %} {% if order.status == PurchaseOrderStatus.PLACED and roles.purchase_order.change %}
if (row.received < row.quantity) { if (row.received < row.quantity) {
html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}'); html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}');
} }

View File

@ -14,7 +14,9 @@ InvenTree | {% trans "Purchase Orders" %}
<div id='table-buttons'> <div id='table-buttons'>
<div class='button-toolbar container-fluid' style='float: right;'> <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> <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'> <div class='filter-list' id='filter-list-purchaseorder'>
<!-- An empty div in which the filter list will be constructed --> <!-- An empty div in which the filter list will be constructed -->
</div> </div>

View File

@ -34,19 +34,17 @@ src="{% static 'img/blank_image.png' %}"
<hr> <hr>
<h4> <h4>
{{ order }} {{ 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> <a href="{% url 'admin:order_salesorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
{% endif %} {% endif %}
</h4> </h4>
<p>{{ order.description }}</p> <p>{{ order.description }}</p>
<div class='btn-row'> <div class='btn-row'>
<div class='btn-group action-buttons'> <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'> <button type='button' class='btn btn-default' id='edit-order' title='Edit order information'>
<span class='fas fa-edit icon-green'></span> <span class='fas fa-edit icon-green'></span>
</button> </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 %} {% if order.status == SalesOrderStatus.PENDING %}
<button type='button' class='btn btn-default' id='ship-order' title='{% trans "Ship order" %}'> <button type='button' class='btn btn-default' id='ship-order' title='{% trans "Ship order" %}'>
<span class='fas fa-paper-plane icon-blue'></span> <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> <span class='fas fa-times-circle icon-red'></span>
</button> </button>
{% endif %} {% 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>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -14,7 +14,9 @@ InvenTree | {% trans "Sales Orders" %}
<div id='table-buttons'> <div id='table-buttons'>
<div class='button-toolbar container-fluid' style='float: right;'> <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> <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'> <div class='filter-list' id='filter-list-salesorder'>
<!-- An empty div in which the filter list will be constructed --> <!-- An empty div in which the filter list will be constructed -->
</div> </div>

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from InvenTree.status_codes import PurchaseOrderStatus from InvenTree.status_codes import PurchaseOrderStatus
@ -32,7 +33,21 @@ class OrderViewTestCase(TestCase):
# Create a user # Create a user
User = get_user_model() 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') self.client.login(username='username', password='password')

View File

@ -28,19 +28,22 @@ from . import forms as order_forms
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.helpers import DownloadFile, str2bool from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.views import InvenTreeRoleMixin
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PurchaseOrderIndex(ListView): class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
""" List view for all purchase orders """ """ List view for all purchase orders """
model = PurchaseOrder model = PurchaseOrder
template_name = 'order/purchase_orders.html' template_name = 'order/purchase_orders.html'
context_object_name = 'orders' context_object_name = 'orders'
role_required = 'purchase_order.view'
def get_queryset(self): def get_queryset(self):
""" Retrieve the list of purchase orders, """ Retrieve the list of purchase orders,
ensure that the most recent ones are returned first. """ ensure that the most recent ones are returned first. """
@ -55,19 +58,21 @@ class PurchaseOrderIndex(ListView):
return ctx return ctx
class SalesOrderIndex(ListView): class SalesOrderIndex(InvenTreeRoleMixin, ListView):
model = SalesOrder model = SalesOrder
template_name = 'order/sales_orders.html' template_name = 'order/sales_orders.html'
context_object_name = 'orders' context_object_name = 'orders'
role_required = 'sales_order.view'
class PurchaseOrderDetail(DetailView): class PurchaseOrderDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for a PurchaseOrder object """ """ Detail view for a PurchaseOrder object """
context_object_name = 'order' context_object_name = 'order'
queryset = PurchaseOrder.objects.all().prefetch_related('lines') queryset = PurchaseOrder.objects.all().prefetch_related('lines')
template_name = 'order/purchase_order_detail.html' template_name = 'order/purchase_order_detail.html'
role_required = 'purchase_order.view'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
@ -75,12 +80,13 @@ class PurchaseOrderDetail(DetailView):
return ctx return ctx
class SalesOrderDetail(DetailView): class SalesOrderDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for a SalesOrder object """ """ Detail view for a SalesOrder object """
context_object_name = 'order' context_object_name = 'order'
queryset = SalesOrder.objects.all().prefetch_related('lines') queryset = SalesOrder.objects.all().prefetch_related('lines')
template_name = 'order/sales_order_detail.html' template_name = 'order/sales_order_detail.html'
role_required = 'sales_order.view'
class PurchaseOrderAttachmentCreate(AjaxCreateView): class PurchaseOrderAttachmentCreate(AjaxCreateView):
@ -92,6 +98,7 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView):
form_class = order_forms.EditPurchaseOrderAttachmentForm form_class = order_forms.EditPurchaseOrderAttachmentForm
ajax_form_title = _("Add Purchase Order Attachment") ajax_form_title = _("Add Purchase Order Attachment")
ajax_template_name = "modal_form.html" ajax_template_name = "modal_form.html"
role_required = 'purchase_order.add'
def post_save(self, **kwargs): def post_save(self, **kwargs):
self.object.user = self.request.user self.object.user = self.request.user
@ -139,6 +146,7 @@ class SalesOrderAttachmentCreate(AjaxCreateView):
model = SalesOrderAttachment model = SalesOrderAttachment
form_class = order_forms.EditSalesOrderAttachmentForm form_class = order_forms.EditSalesOrderAttachmentForm
ajax_form_title = _('Add Sales Order Attachment') ajax_form_title = _('Add Sales Order Attachment')
role_required = 'sales_order.add'
def post_save(self, **kwargs): def post_save(self, **kwargs):
self.object.user = self.request.user self.object.user = self.request.user
@ -174,6 +182,7 @@ class PurchaseOrderAttachmentEdit(AjaxUpdateView):
model = PurchaseOrderAttachment model = PurchaseOrderAttachment
form_class = order_forms.EditPurchaseOrderAttachmentForm form_class = order_forms.EditPurchaseOrderAttachmentForm
ajax_form_title = _("Edit Attachment") ajax_form_title = _("Edit Attachment")
role_required = 'purchase_order.change'
def get_data(self): def get_data(self):
return { return {
@ -195,6 +204,7 @@ class SalesOrderAttachmentEdit(AjaxUpdateView):
model = SalesOrderAttachment model = SalesOrderAttachment
form_class = order_forms.EditSalesOrderAttachmentForm form_class = order_forms.EditSalesOrderAttachmentForm
ajax_form_title = _("Edit Attachment") ajax_form_title = _("Edit Attachment")
role_required = 'sales_order.change'
def get_data(self): def get_data(self):
return { return {
@ -216,6 +226,7 @@ class PurchaseOrderAttachmentDelete(AjaxDeleteView):
ajax_form_title = _("Delete Attachment") ajax_form_title = _("Delete Attachment")
ajax_template_name = "order/delete_attachment.html" ajax_template_name = "order/delete_attachment.html"
context_object_name = "attachment" context_object_name = "attachment"
role_required = 'purchase_order.delete'
def get_data(self): def get_data(self):
return { return {
@ -230,6 +241,7 @@ class SalesOrderAttachmentDelete(AjaxDeleteView):
ajax_form_title = _("Delete Attachment") ajax_form_title = _("Delete Attachment")
ajax_template_name = "order/delete_attachment.html" ajax_template_name = "order/delete_attachment.html"
context_object_name = "attachment" context_object_name = "attachment"
role_required = 'sales_order.delete'
def get_data(self): def get_data(self):
return { return {
@ -237,12 +249,13 @@ class SalesOrderAttachmentDelete(AjaxDeleteView):
} }
class PurchaseOrderNotes(UpdateView): class PurchaseOrderNotes(InvenTreeRoleMixin, UpdateView):
""" View for updating the 'notes' field of a PurchaseOrder """ """ View for updating the 'notes' field of a PurchaseOrder """
context_object_name = 'order' context_object_name = 'order'
template_name = 'order/order_notes.html' template_name = 'order/order_notes.html'
model = PurchaseOrder model = PurchaseOrder
role_required = 'purchase_order.view'
fields = ['notes'] fields = ['notes']
@ -259,12 +272,13 @@ class PurchaseOrderNotes(UpdateView):
return ctx return ctx
class SalesOrderNotes(UpdateView): class SalesOrderNotes(InvenTreeRoleMixin, UpdateView):
""" View for editing the 'notes' field of a SalesORder """ """ View for editing the 'notes' field of a SalesORder """
context_object_name = 'order' context_object_name = 'order'
template_name = 'order/sales_order_notes.html' template_name = 'order/sales_order_notes.html'
model = SalesOrder model = SalesOrder
role_required = 'sales_order.view'
fields = ['notes'] fields = ['notes']
@ -286,6 +300,7 @@ class PurchaseOrderCreate(AjaxCreateView):
model = PurchaseOrder model = PurchaseOrder
ajax_form_title = _("Create Purchase Order") ajax_form_title = _("Create Purchase Order")
form_class = order_forms.EditPurchaseOrderForm form_class = order_forms.EditPurchaseOrderForm
role_required = 'purchase_order.add'
def get_initial(self): def get_initial(self):
initials = super().get_initial().copy() initials = super().get_initial().copy()
@ -317,6 +332,7 @@ class SalesOrderCreate(AjaxCreateView):
model = SalesOrder model = SalesOrder
ajax_form_title = _("Create Sales Order") ajax_form_title = _("Create Sales Order")
form_class = order_forms.EditSalesOrderForm form_class = order_forms.EditSalesOrderForm
role_required = 'sales_order.add'
def get_initial(self): def get_initial(self):
initials = super().get_initial().copy() initials = super().get_initial().copy()
@ -347,6 +363,7 @@ class PurchaseOrderEdit(AjaxUpdateView):
model = PurchaseOrder model = PurchaseOrder
ajax_form_title = _('Edit Purchase Order') ajax_form_title = _('Edit Purchase Order')
form_class = order_forms.EditPurchaseOrderForm form_class = order_forms.EditPurchaseOrderForm
role_required = 'purchase_order.change'
def get_form(self): def get_form(self):
@ -367,6 +384,7 @@ class SalesOrderEdit(AjaxUpdateView):
model = SalesOrder model = SalesOrder
ajax_form_title = _('Edit Sales Order') ajax_form_title = _('Edit Sales Order')
form_class = order_forms.EditSalesOrderForm form_class = order_forms.EditSalesOrderForm
role_required = 'sales_order.change'
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
@ -384,6 +402,7 @@ class PurchaseOrderCancel(AjaxUpdateView):
ajax_form_title = _('Cancel Order') ajax_form_title = _('Cancel Order')
ajax_template_name = 'order/order_cancel.html' ajax_template_name = 'order/order_cancel.html'
form_class = order_forms.CancelPurchaseOrderForm form_class = order_forms.CancelPurchaseOrderForm
role_required = 'purchase_order.change'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" Mark the PO as 'CANCELLED' """ """ Mark the PO as 'CANCELLED' """
@ -417,6 +436,7 @@ class SalesOrderCancel(AjaxUpdateView):
ajax_form_title = _("Cancel sales order") ajax_form_title = _("Cancel sales order")
ajax_template_name = "order/sales_order_cancel.html" ajax_template_name = "order/sales_order_cancel.html"
form_class = order_forms.CancelSalesOrderForm form_class = order_forms.CancelSalesOrderForm
role_required = 'sales_order.change'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -451,6 +471,7 @@ class PurchaseOrderIssue(AjaxUpdateView):
ajax_form_title = _('Issue Order') ajax_form_title = _('Issue Order')
ajax_template_name = "order/order_issue.html" ajax_template_name = "order/order_issue.html"
form_class = order_forms.IssuePurchaseOrderForm form_class = order_forms.IssuePurchaseOrderForm
role_required = 'purchase_order.change'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" Mark the purchase order as 'PLACED' """ """ Mark the purchase order as 'PLACED' """
@ -486,6 +507,7 @@ class PurchaseOrderComplete(AjaxUpdateView):
ajax_template_name = "order/order_complete.html" ajax_template_name = "order/order_complete.html"
ajax_form_title = _("Complete Order") ajax_form_title = _("Complete Order")
context_object_name = 'order' context_object_name = 'order'
role_required = 'purchase_order.change'
def get_context_data(self): def get_context_data(self):
@ -520,6 +542,7 @@ class SalesOrderShip(AjaxUpdateView):
context_object_name = 'order' context_object_name = 'order'
ajax_template_name = 'order/sales_order_ship.html' ajax_template_name = 'order/sales_order_ship.html'
ajax_form_title = _('Ship Order') ajax_form_title = _('Ship Order')
role_required = 'sales_order.change'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -563,6 +586,7 @@ class PurchaseOrderExport(AjaxView):
""" """
model = PurchaseOrder model = PurchaseOrder
role_required = 'purchase_order.view'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -594,6 +618,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
form_class = order_forms.ReceivePurchaseOrderForm form_class = order_forms.ReceivePurchaseOrderForm
ajax_form_title = _("Receive Parts") ajax_form_title = _("Receive Parts")
ajax_template_name = "order/receive_parts.html" ajax_template_name = "order/receive_parts.html"
role_required = 'purchase_order.change'
# Where the parts will be going (selected in POST request) # Where the parts will be going (selected in POST request)
destination = None destination = None
@ -779,6 +804,11 @@ class OrderParts(AjaxView):
ajax_form_title = _("Order Parts") ajax_form_title = _("Order Parts")
ajax_template_name = 'order/order_wizard/select_parts.html' ajax_template_name = 'order/order_wizard/select_parts.html'
role_required = [
'part.view',
'purchase_order.change',
]
# List of Parts we wish to order # List of Parts we wish to order
parts = [] parts = []
suppliers = [] suppliers = []
@ -1085,6 +1115,7 @@ class POLineItemCreate(AjaxCreateView):
context_object_name = 'line' context_object_name = 'line'
form_class = order_forms.EditPurchaseOrderLineItemForm form_class = order_forms.EditPurchaseOrderLineItemForm
ajax_form_title = _('Add Line Item') ajax_form_title = _('Add Line Item')
role_required = 'purchase_order.add'
def post(self, request, *arg, **kwargs): def post(self, request, *arg, **kwargs):
@ -1199,6 +1230,7 @@ class SOLineItemCreate(AjaxCreateView):
context_order_name = 'line' context_order_name = 'line'
form_class = order_forms.EditSalesOrderLineItemForm form_class = order_forms.EditSalesOrderLineItemForm
ajax_form_title = _('Add Line Item') ajax_form_title = _('Add Line Item')
role_required = 'sales_order.add'
def get_form(self, *args, **kwargs): def get_form(self, *args, **kwargs):
@ -1250,6 +1282,7 @@ class SOLineItemEdit(AjaxUpdateView):
model = SalesOrderLineItem model = SalesOrderLineItem
form_class = order_forms.EditSalesOrderLineItemForm form_class = order_forms.EditSalesOrderLineItemForm
ajax_form_title = _('Edit Line Item') ajax_form_title = _('Edit Line Item')
role_required = 'sales_order.change'
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
@ -1268,6 +1301,7 @@ class POLineItemEdit(AjaxUpdateView):
form_class = order_forms.EditPurchaseOrderLineItemForm form_class = order_forms.EditPurchaseOrderLineItemForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Line Item') ajax_form_title = _('Edit Line Item')
role_required = 'purchase_order.change'
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
@ -1285,7 +1319,8 @@ class POLineItemDelete(AjaxDeleteView):
model = PurchaseOrderLineItem model = PurchaseOrderLineItem
ajax_form_title = _('Delete Line Item') ajax_form_title = _('Delete Line Item')
ajax_template_name = 'order/po_lineitem_delete.html' ajax_template_name = 'order/po_lineitem_delete.html'
role_required = 'purchase_order.delete'
def get_data(self): def get_data(self):
return { return {
'danger': _('Deleted line item'), 'danger': _('Deleted line item'),
@ -1297,6 +1332,7 @@ class SOLineItemDelete(AjaxDeleteView):
model = SalesOrderLineItem model = SalesOrderLineItem
ajax_form_title = _("Delete Line Item") ajax_form_title = _("Delete Line Item")
ajax_template_name = "order/so_lineitem_delete.html" ajax_template_name = "order/so_lineitem_delete.html"
role_required = 'sales_order.delete'
def get_data(self): def get_data(self):
return { return {
@ -1310,6 +1346,7 @@ class SalesOrderAllocationCreate(AjaxCreateView):
model = SalesOrderAllocation model = SalesOrderAllocation
form_class = order_forms.EditSalesOrderAllocationForm form_class = order_forms.EditSalesOrderAllocationForm
ajax_form_title = _('Allocate Stock to Order') ajax_form_title = _('Allocate Stock to Order')
role_required = 'sales_order.add'
def get_initial(self): def get_initial(self):
initials = super().get_initial().copy() initials = super().get_initial().copy()
@ -1379,6 +1416,7 @@ class SalesOrderAllocationEdit(AjaxUpdateView):
model = SalesOrderAllocation model = SalesOrderAllocation
form_class = order_forms.EditSalesOrderAllocationForm form_class = order_forms.EditSalesOrderAllocationForm
ajax_form_title = _('Edit Allocation Quantity') ajax_form_title = _('Edit Allocation Quantity')
role_required = 'sales_order.change'
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
@ -1396,3 +1434,4 @@ class SalesOrderAllocationDelete(AjaxDeleteView):
ajax_form_title = _("Remove allocation") ajax_form_title = _("Remove allocation")
context_object_name = 'allocation' context_object_name = 'allocation'
ajax_template_name = "order/so_allocation_delete.html" ajax_template_name = "order/so_allocation_delete.html"
role_required = 'sales_order.delete'

View File

@ -44,6 +44,10 @@ class PartCategoryTree(TreeSerializer):
def get_items(self): def get_items(self):
return PartCategory.objects.all().prefetch_related('parts', 'children') return PartCategory.objects.all().prefetch_related('parts', 'children')
permission_classes = [
permissions.IsAuthenticated,
]
class CategoryList(generics.ListCreateAPIView): class CategoryList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartCategory objects. """ API endpoint for accessing a list of PartCategory objects.
@ -55,10 +59,6 @@ class CategoryList(generics.ListCreateAPIView):
queryset = PartCategory.objects.all() queryset = PartCategory.objects.all()
serializer_class = part_serializers.CategorySerializer serializer_class = part_serializers.CategorySerializer
permission_classes = [
permissions.IsAuthenticated,
]
def get_queryset(self): def get_queryset(self):
""" """
Custom filtering: Custom filtering:
@ -119,10 +119,6 @@ class PartSalePriceList(generics.ListCreateAPIView):
queryset = PartSellPriceBreak.objects.all() queryset = PartSellPriceBreak.objects.all()
serializer_class = part_serializers.PartSalePriceSerializer serializer_class = part_serializers.PartSalePriceSerializer
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend DjangoFilterBackend
] ]
@ -182,8 +178,6 @@ class PartTestTemplateList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [permissions.IsAuthenticated]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.OrderingFilter, filters.OrderingFilter,
@ -221,10 +215,6 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
queryset = Part.objects.all() queryset = Part.objects.all()
serializer_class = part_serializers.PartThumbSerializerUpdate serializer_class = part_serializers.PartThumbSerializerUpdate
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend DjangoFilterBackend
] ]
@ -246,10 +236,6 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated,
]
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
try: try:
@ -580,10 +566,6 @@ class PartList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -676,10 +658,6 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
queryset = PartParameterTemplate.objects.all() queryset = PartParameterTemplate.objects.all()
serializer_class = part_serializers.PartParameterTemplateSerializer serializer_class = part_serializers.PartParameterTemplateSerializer
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
filters.OrderingFilter, filters.OrderingFilter,
] ]
@ -699,10 +677,6 @@ class PartParameterList(generics.ListCreateAPIView):
queryset = PartParameter.objects.all() queryset = PartParameter.objects.all()
serializer_class = part_serializers.PartParameterSerializer serializer_class = part_serializers.PartParameterSerializer
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend DjangoFilterBackend
] ]
@ -796,10 +770,6 @@ class BomList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -816,10 +786,6 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
serializer_class = part_serializers.BomItemSerializer serializer_class = part_serializers.BomItemSerializer
permission_classes = [
permissions.IsAuthenticated,
]
class BomItemValidate(generics.UpdateAPIView): class BomItemValidate(generics.UpdateAPIView):
""" API endpoint for validating a BomItem """ """ 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 "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> <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 %} {% 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> <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 %} {% 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> <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 %}
{% 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> <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 %} {% endif %}
</div> </div>

View File

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

View File

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

View File

@ -29,7 +29,9 @@
<h4>{% trans "Part Notes" %}</h4> <h4>{% trans "Part Notes" %}</h4>
</div> </div>
<div class='col-sm-6'> <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> <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>
</div> </div>
<hr> <hr>

View File

@ -10,7 +10,9 @@
<div id='button-toolbar'> <div id='button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'> <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> <button title='{% trans "Add new parameter" %}' class='btn btn-success' id='param-create'>{% trans "New Parameter" %}</button>
{% endif %}
</div> </div>
</div> </div>
@ -30,8 +32,12 @@
<td> <td>
{{ param.template.units }} {{ param.template.units }}
<div class='btn-group' style='float: right;'> <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> <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> <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> </div>
</td> </td>
</tr> </tr>
@ -48,6 +54,7 @@
$('#param-table').inventreeTable({ $('#param-table').inventreeTable({
}); });
{% if roles.part.add %}
$('#param-create').click(function() { $('#param-create').click(function() {
launchModalForm("{% url 'part-param-create' %}?part={{ part.id }}", { launchModalForm("{% url 'part-param-create' %}?part={{ part.id }}", {
reload: true, reload: true,
@ -59,6 +66,7 @@
}], }],
}); });
}); });
{% endif %}
$('.param-edit').click(function() { $('.param-edit').click(function() {
var button = $(this); var button = $(this);

View File

@ -28,7 +28,7 @@
<div class="media-body"> <div class="media-body">
<h3> <h3>
{{ part.full_name }} {{ 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> <a href="{% url 'admin:part_part_change' part.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %} {% endif %}
{% if not part.active %} {% if not part.active %}
@ -56,26 +56,36 @@
<button type='button' class='btn btn-default' id='price-button' title='{% trans "Show pricing information" %}'> <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'/> <span id='part-price-icon' class='fas fa-dollar-sign'/>
</button> </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'/> <span class='fas fa-clipboard-list'/>
</button> </button>
{% endif %}
{% if part.purchaseable %} {% 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'/> <span id='part-order-icon' class='fas fa-shopping-cart'/>
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
<!-- Part actions --> <!-- Part actions -->
{% if roles.part.add or roles.part.change or roles.part.delete %}
<div class='btn-group'> <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> <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'> <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> <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> <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> <li><a href='#' id='part-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete part" %}</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
{% endif %}
</div> </div>
<table class='table table-condensed'> <table class='table table-condensed'>
<col width='25'> <col width='25'>
@ -274,6 +284,7 @@
}); });
}); });
{% if roles.part.change %}
$("#part-edit").click(function() { $("#part-edit").click(function() {
launchModalForm( launchModalForm(
"{% url 'part-edit' part.id %}", "{% url 'part-edit' part.id %}",
@ -282,6 +293,7 @@
} }
); );
}); });
{% endif %}
$("#part-order").click(function() { $("#part-order").click(function() {
launchModalForm("{% url 'order-parts' %}", { launchModalForm("{% url 'order-parts' %}", {
@ -292,6 +304,7 @@
}); });
}); });
{% if roles.part.add %}
$("#part-duplicate").click(function() { $("#part-duplicate").click(function() {
launchModalForm( launchModalForm(
"{% url 'part-duplicate' part.id %}", "{% 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() { $("#part-delete").click(function() {
launchModalForm( launchModalForm(
"{% url 'part-delete' part.id %}", "{% url 'part-delete' part.id %}",

View File

@ -26,14 +26,17 @@
{% if part.assembly %} {% if part.assembly %}
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}> <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> <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 %}> <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 %} {% endif %}
{% if part.component or part.used_in_count > 0 %} {% if part.component or part.used_in_count > 0 %}
<li{% ifequal tab 'used' %} class="active"{% endifequal %}> <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> <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 %} {% endif %}
{% if part.purchaseable %} {% if part.purchaseable and roles.purchase_order.view %}
{% if part.is_template == False %} {% if part.is_template == False %}
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}> <li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
<a href="{% url 'part-suppliers' part.id %}">{% trans "Suppliers" %} <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> <a href="{% url 'part-orders' part.id %}">{% trans "Purchase Orders" %} <span class='badge'>{{ part.purchase_orders|length }}</span></a>
</li> </li>
{% endif %} {% endif %}
{% if part.salable %} {% if part.salable and roles.sales_order.view %}
<li {% if tab == 'sales-prices' %}class='active'{% endif %}> <li {% if tab == 'sales-prices' %}class='active'{% endif %}>
<a href="{% url 'part-sale-prices' part.id %}">{% trans "Sale Price" %}</a> <a href="{% url 'part-sale-prices' part.id %}">{% trans "Sale Price" %}</a>
</li> </li>

View File

@ -3,6 +3,7 @@ from rest_framework import status
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from part.models import Part from part.models import Part
from stock.models import StockItem from stock.models import StockItem
@ -29,7 +30,26 @@ class PartAPITest(APITestCase):
def setUp(self): def setUp(self):
# Create a user for auth # Create a user for auth
User = get_user_model() 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') self.client.login(username='testuser', password='password')

View File

@ -3,6 +3,7 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from .models import Part from .models import Part
@ -23,7 +24,24 @@ class PartViewTestCase(TestCase):
# Create a user # Create a user
User = get_user_model() 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') self.client.login(username='username', password='password')
@ -140,12 +158,14 @@ class PartTests(PartViewTestCase):
""" Tests for Part forms """ """ Tests for Part forms """
def test_part_edit(self): def test_part_edit(self):
response = self.client.get(reverse('part-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(reverse('part-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
keys = response.context.keys() keys = response.context.keys()
data = str(response.content) data = str(response.content)
self.assertEqual(response.status_code, 200)
self.assertIn('part', keys) self.assertIn('part', keys)
self.assertIn('csrf_token', 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') response = self.client.get(reverse('part-attachment-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# TODO - Create a new attachment using this view
def test_invalid_create(self): def test_invalid_create(self):
""" test creation of an attachment for an invalid part """ """ 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 AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView from InvenTree.views import QRCodeView
from InvenTree.views import InvenTreeRoleMixin
from InvenTree.helpers import DownloadFile, str2bool from InvenTree.helpers import DownloadFile, str2bool
class PartIndex(ListView): class PartIndex(InvenTreeRoleMixin, ListView):
""" View for displaying list of Part objects """ View for displaying list of Part objects
""" """
model = Part model = Part
template_name = 'part/category.html' template_name = 'part/category.html'
context_object_name = 'parts' context_object_name = 'parts'
role_required = 'part.view'
def get_queryset(self): def get_queryset(self):
return Part.objects.all().select_related('category') return Part.objects.all().select_related('category')
@ -76,6 +80,8 @@ class PartAttachmentCreate(AjaxCreateView):
ajax_form_title = _("Add part attachment") ajax_form_title = _("Add part attachment")
ajax_template_name = "modal_form.html" ajax_template_name = "modal_form.html"
role_required = 'part.add'
def post_save(self): def post_save(self):
""" Record the user that uploaded the attachment """ """ Record the user that uploaded the attachment """
self.object.user = self.request.user self.object.user = self.request.user
@ -123,6 +129,8 @@ class PartAttachmentEdit(AjaxUpdateView):
form_class = part_forms.EditPartAttachmentForm form_class = part_forms.EditPartAttachmentForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit attachment') ajax_form_title = _('Edit attachment')
role_required = 'part.change'
def get_data(self): def get_data(self):
return { return {
@ -145,6 +153,8 @@ class PartAttachmentDelete(AjaxDeleteView):
ajax_template_name = "attachment_delete.html" ajax_template_name = "attachment_delete.html"
context_object_name = "attachment" context_object_name = "attachment"
role_required = 'part.delete'
def get_data(self): def get_data(self):
return { return {
'danger': _('Deleted part attachment') 'danger': _('Deleted part attachment')
@ -157,6 +167,8 @@ class PartTestTemplateCreate(AjaxCreateView):
model = PartTestTemplate model = PartTestTemplate
form_class = part_forms.EditPartTestTemplateForm form_class = part_forms.EditPartTestTemplateForm
ajax_form_title = _("Create Test Template") ajax_form_title = _("Create Test Template")
role_required = 'part.add'
def get_initial(self): def get_initial(self):
@ -185,6 +197,8 @@ class PartTestTemplateEdit(AjaxUpdateView):
form_class = part_forms.EditPartTestTemplateForm form_class = part_forms.EditPartTestTemplateForm
ajax_form_title = _("Edit Test Template") ajax_form_title = _("Edit Test Template")
role_required = 'part.change'
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
@ -199,6 +213,8 @@ class PartTestTemplateDelete(AjaxDeleteView):
model = PartTestTemplate model = PartTestTemplate
ajax_form_title = _("Delete Test Template") ajax_form_title = _("Delete Test Template")
role_required = 'part.delete'
class PartSetCategory(AjaxUpdateView): class PartSetCategory(AjaxUpdateView):
""" View for settings the part category for multiple parts at once """ """ View for settings the part category for multiple parts at once """
@ -207,6 +223,8 @@ class PartSetCategory(AjaxUpdateView):
ajax_form_title = _('Set Part Category') ajax_form_title = _('Set Part Category')
form_class = part_forms.SetPartCategoryForm form_class = part_forms.SetPartCategoryForm
role_required = 'part.change'
category = None category = None
parts = [] parts = []
@ -290,6 +308,8 @@ class MakePartVariant(AjaxCreateView):
ajax_form_title = _('Create Variant') ajax_form_title = _('Create Variant')
ajax_template_name = 'part/variant_part.html' ajax_template_name = 'part/variant_part.html'
role_required = 'part.add'
def get_part_template(self): def get_part_template(self):
return get_object_or_404(Part, id=self.kwargs['pk']) return get_object_or_404(Part, id=self.kwargs['pk'])
@ -368,6 +388,8 @@ class PartDuplicate(AjaxCreateView):
ajax_form_title = _("Duplicate Part") ajax_form_title = _("Duplicate Part")
ajax_template_name = "part/copy_part.html" ajax_template_name = "part/copy_part.html"
role_required = 'part.add'
def get_data(self): def get_data(self):
return { return {
'success': _('Copied part') 'success': _('Copied part')
@ -491,6 +513,8 @@ class PartCreate(AjaxCreateView):
ajax_form_title = _('Create new part') ajax_form_title = _('Create new part')
ajax_template_name = 'part/create_part.html' ajax_template_name = 'part/create_part.html'
role_required = 'part.add'
def get_data(self): def get_data(self):
return { return {
'success': _("Created new part"), 'success': _("Created new part"),
@ -613,6 +637,8 @@ class PartNotes(UpdateView):
template_name = 'part/notes.html' template_name = 'part/notes.html'
model = Part model = Part
role_required = 'part.change'
fields = ['notes'] fields = ['notes']
def get_success_url(self): def get_success_url(self):
@ -634,7 +660,7 @@ class PartNotes(UpdateView):
return ctx return ctx
class PartDetail(DetailView): class PartDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for Part object """ Detail view for Part object
""" """
@ -642,6 +668,8 @@ class PartDetail(DetailView):
queryset = Part.objects.all().select_related('category') queryset = Part.objects.all().select_related('category')
template_name = 'part/detail.html' template_name = 'part/detail.html'
role_required = 'part.view'
# Add in some extra context information based on query params # Add in some extra context information based on query params
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Provide extra context data to template """ Provide extra context data to template
@ -706,6 +734,8 @@ class PartQRCode(QRCodeView):
ajax_form_title = _("Part QR Code") ajax_form_title = _("Part QR Code")
role_required = 'part.view'
def get_qr_data(self): def get_qr_data(self):
""" Generate QR code data for the Part """ """ Generate QR code data for the Part """
@ -722,8 +752,11 @@ class PartImageUpload(AjaxUpdateView):
model = Part model = Part
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Upload Part Image') ajax_form_title = _('Upload Part Image')
form_class = part_forms.PartImageForm form_class = part_forms.PartImageForm
role_required = 'part.change'
def get_data(self): def get_data(self):
return { return {
'success': _('Updated part image'), 'success': _('Updated part image'),
@ -737,6 +770,8 @@ class PartImageSelect(AjaxUpdateView):
ajax_template_name = 'part/select_image.html' ajax_template_name = 'part/select_image.html'
ajax_form_title = _('Select Part Image') ajax_form_title = _('Select Part Image')
role_required = 'part.change'
fields = [ fields = [
'image', 'image',
] ]
@ -778,6 +813,8 @@ class PartEdit(AjaxUpdateView):
ajax_form_title = _('Edit Part Properties') ajax_form_title = _('Edit Part Properties')
context_object_name = 'part' context_object_name = 'part'
role_required = 'part.change'
def get_form(self): def get_form(self):
""" Create form for Part editing. """ Create form for Part editing.
Overrides default get_form() method to limit the choices Overrides default get_form() method to limit the choices
@ -802,6 +839,8 @@ class BomValidate(AjaxUpdateView):
context_object_name = 'part' context_object_name = 'part'
form_class = part_forms.BomValidateForm form_class = part_forms.BomValidateForm
role_required = 'part.change'
def get_context(self): def get_context(self):
return { return {
'part': self.get_object(), 'part': self.get_object(),
@ -832,7 +871,7 @@ class BomValidate(AjaxUpdateView):
return self.renderJsonResponse(request, form, data, context=self.get_context()) 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. """ View for uploading a BOM file, and handling BOM data importing.
The BOM upload process is as follows: The BOM upload process is as follows:
@ -868,6 +907,8 @@ class BomUpload(FormView):
missing_columns = [] missing_columns = []
allowed_parts = [] allowed_parts = []
role_required = ('part.change', 'part.add')
def get_success_url(self): def get_success_url(self):
part = self.get_object() part = self.get_object()
return reverse('upload-bom', kwargs={'pk': part.id}) return reverse('upload-bom', kwargs={'pk': part.id})
@ -1466,6 +1507,8 @@ class BomUpload(FormView):
class PartExport(AjaxView): class PartExport(AjaxView):
""" Export a CSV file containing information on multiple parts """ """ Export a CSV file containing information on multiple parts """
role_required = 'part.view'
def get_parts(self, request): def get_parts(self, request):
""" Extract part list from the POST parameters. """ Extract part list from the POST parameters.
Parts can be supplied as: 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 - File format should be passed as a query param e.g. ?format=csv
""" """
role_required = 'part.view'
model = Part model = Part
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -1596,6 +1641,8 @@ class BomExport(AjaxView):
form_class = part_forms.BomExportForm form_class = part_forms.BomExportForm
ajax_form_title = _("Export Bill of Materials") ajax_form_title = _("Export Bill of Materials")
role_required = 'part.view'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return self.renderJsonResponse(request, self.form_class()) return self.renderJsonResponse(request, self.form_class())
@ -1645,6 +1692,8 @@ class PartDelete(AjaxDeleteView):
ajax_form_title = _('Confirm Part Deletion') ajax_form_title = _('Confirm Part Deletion')
context_object_name = 'part' context_object_name = 'part'
role_required = 'part.delete'
success_url = '/part/' success_url = '/part/'
def get_data(self): def get_data(self):
@ -1661,6 +1710,8 @@ class PartPricing(AjaxView):
ajax_form_title = _("Part Pricing") ajax_form_title = _("Part Pricing")
form_class = part_forms.PartPriceForm form_class = part_forms.PartPriceForm
role_required = ['sales_order.view', 'part.view']
def get_part(self): def get_part(self):
try: try:
return Part.objects.get(id=self.kwargs['pk']) return Part.objects.get(id=self.kwargs['pk'])
@ -1778,6 +1829,8 @@ class PartPricing(AjaxView):
class PartParameterTemplateCreate(AjaxCreateView): class PartParameterTemplateCreate(AjaxCreateView):
""" View for creating a new PartParameterTemplate """ """ View for creating a new PartParameterTemplate """
role_required = 'part.add'
model = PartParameterTemplate model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm form_class = part_forms.EditPartParameterTemplateForm
ajax_form_title = _('Create Part Parameter Template') ajax_form_title = _('Create Part Parameter Template')
@ -1786,6 +1839,8 @@ class PartParameterTemplateCreate(AjaxCreateView):
class PartParameterTemplateEdit(AjaxUpdateView): class PartParameterTemplateEdit(AjaxUpdateView):
""" View for editing a PartParameterTemplate """ """ View for editing a PartParameterTemplate """
role_required = 'part.change'
model = PartParameterTemplate model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm form_class = part_forms.EditPartParameterTemplateForm
ajax_form_title = _('Edit Part Parameter Template') ajax_form_title = _('Edit Part Parameter Template')
@ -1794,6 +1849,8 @@ class PartParameterTemplateEdit(AjaxUpdateView):
class PartParameterTemplateDelete(AjaxDeleteView): class PartParameterTemplateDelete(AjaxDeleteView):
""" View for deleting an existing PartParameterTemplate """ """ View for deleting an existing PartParameterTemplate """
role_required = 'part.delete'
model = PartParameterTemplate model = PartParameterTemplate
ajax_form_title = _("Delete Part Parameter Template") ajax_form_title = _("Delete Part Parameter Template")
@ -1801,6 +1858,8 @@ class PartParameterTemplateDelete(AjaxDeleteView):
class PartParameterCreate(AjaxCreateView): class PartParameterCreate(AjaxCreateView):
""" View for creating a new PartParameter """ """ View for creating a new PartParameter """
role_required = 'part.add'
model = PartParameter model = PartParameter
form_class = part_forms.EditPartParameterForm form_class = part_forms.EditPartParameterForm
ajax_form_title = _('Create Part Parameter') ajax_form_title = _('Create Part Parameter')
@ -1851,6 +1910,8 @@ class PartParameterCreate(AjaxCreateView):
class PartParameterEdit(AjaxUpdateView): class PartParameterEdit(AjaxUpdateView):
""" View for editing a PartParameter """ """ View for editing a PartParameter """
role_required = 'part.change'
model = PartParameter model = PartParameter
form_class = part_forms.EditPartParameterForm form_class = part_forms.EditPartParameterForm
ajax_form_title = _('Edit Part Parameter') ajax_form_title = _('Edit Part Parameter')
@ -1865,12 +1926,14 @@ class PartParameterEdit(AjaxUpdateView):
class PartParameterDelete(AjaxDeleteView): class PartParameterDelete(AjaxDeleteView):
""" View for deleting a PartParameter """ """ View for deleting a PartParameter """
role_required = 'part.delete'
model = PartParameter model = PartParameter
ajax_template_name = 'part/param_delete.html' ajax_template_name = 'part/param_delete.html'
ajax_form_title = _('Delete Part Parameter') ajax_form_title = _('Delete Part Parameter')
class CategoryDetail(DetailView): class CategoryDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for PartCategory """ """ Detail view for PartCategory """
model = PartCategory model = PartCategory
@ -1878,6 +1941,8 @@ class CategoryDetail(DetailView):
queryset = PartCategory.objects.all().prefetch_related('children') queryset = PartCategory.objects.all().prefetch_related('children')
template_name = 'part/category_partlist.html' template_name = 'part/category_partlist.html'
role_required = 'part.view'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CategoryDetail, self).get_context_data(**kwargs).copy() context = super(CategoryDetail, self).get_context_data(**kwargs).copy()
@ -1926,6 +1991,8 @@ class CategoryEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Part Category') ajax_form_title = _('Edit Part Category')
role_required = 'part.change'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CategoryEdit, self).get_context_data(**kwargs).copy() context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
@ -1963,6 +2030,8 @@ class CategoryDelete(AjaxDeleteView):
context_object_name = 'category' context_object_name = 'category'
success_url = '/part/' success_url = '/part/'
role_required = 'part.delete'
def get_data(self): def get_data(self):
return { return {
'danger': _('Part category was deleted'), 'danger': _('Part category was deleted'),
@ -1977,6 +2046,8 @@ class CategoryCreate(AjaxCreateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
form_class = part_forms.EditCategoryForm form_class = part_forms.EditCategoryForm
role_required = 'part.add'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add extra context data to template. """ Add extra context data to template.
@ -2012,12 +2083,14 @@ class CategoryCreate(AjaxCreateView):
return initials return initials
class BomItemDetail(DetailView): class BomItemDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for BomItem """ """ Detail view for BomItem """
context_object_name = 'item' context_object_name = 'item'
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
template_name = 'part/bom-detail.html' template_name = 'part/bom-detail.html'
role_required = 'part.view'
class BomItemCreate(AjaxCreateView): class BomItemCreate(AjaxCreateView):
""" Create view for making a new BomItem object """ """ Create view for making a new BomItem object """
@ -2026,6 +2099,8 @@ class BomItemCreate(AjaxCreateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Create BOM item') ajax_form_title = _('Create BOM item')
role_required = 'part.add'
def get_form(self): def get_form(self):
""" Override get_form() method to reduce Part selection options. """ Override get_form() method to reduce Part selection options.
@ -2092,6 +2167,8 @@ class BomItemEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit BOM item') ajax_form_title = _('Edit BOM item')
role_required = 'part.change'
def get_form(self): def get_form(self):
""" Override get_form() method to filter part selection options """ Override get_form() method to filter part selection options
@ -2140,6 +2217,8 @@ class BomItemDelete(AjaxDeleteView):
context_object_name = 'item' context_object_name = 'item'
ajax_form_title = _('Confim BOM item deletion') ajax_form_title = _('Confim BOM item deletion')
role_required = 'part.delete'
class PartSalePriceBreakCreate(AjaxCreateView): class PartSalePriceBreakCreate(AjaxCreateView):
""" View for creating a sale price break for a part """ """ View for creating a sale price break for a part """
@ -2147,6 +2226,8 @@ class PartSalePriceBreakCreate(AjaxCreateView):
model = PartSellPriceBreak model = PartSellPriceBreak
form_class = part_forms.EditPartSalePriceBreakForm form_class = part_forms.EditPartSalePriceBreakForm
ajax_form_title = _('Add Price Break') ajax_form_title = _('Add Price Break')
role_required = 'part.add'
def get_data(self): def get_data(self):
return { return {
@ -2197,6 +2278,8 @@ class PartSalePriceBreakEdit(AjaxUpdateView):
form_class = part_forms.EditPartSalePriceBreakForm form_class = part_forms.EditPartSalePriceBreakForm
ajax_form_title = _('Edit Price Break') ajax_form_title = _('Edit Price Break')
role_required = 'part.change'
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
@ -2211,3 +2294,5 @@ class PartSalePriceBreakDelete(AjaxDeleteView):
model = PartSellPriceBreak model = PartSellPriceBreak
ajax_form_title = _("Delete Price Break") ajax_form_title = _("Delete Price Break")
ajax_template_name = "modal_delete_form.html" ajax_template_name = "modal_delete_form.html"
role_required = 'part.delete'

View File

@ -52,6 +52,10 @@ class StockCategoryTree(TreeSerializer):
def get_items(self): def get_items(self):
return StockLocation.objects.all().prefetch_related('stock_items', 'children') return StockLocation.objects.all().prefetch_related('stock_items', 'children')
permission_classes = [
permissions.IsAuthenticated,
]
class StockDetail(generics.RetrieveUpdateDestroyAPIView): class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" API detail endpoint for Stock object """ API detail endpoint for Stock object
@ -68,7 +72,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = StockItem.objects.all() queryset = StockItem.objects.all()
serializer_class = StockItemSerializer serializer_class = StockItemSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
@ -289,10 +292,6 @@ class StockLocationList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -695,10 +694,6 @@ class StockList(generics.ListCreateAPIView):
return queryset return queryset
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -744,10 +739,6 @@ class StockItemTestResultList(generics.ListCreateAPIView):
queryset = StockItemTestResult.objects.all() queryset = StockItemTestResult.objects.all()
serializer_class = StockItemTestResultSerializer serializer_class = StockItemTestResultSerializer
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -799,7 +790,6 @@ class StockTrackingList(generics.ListCreateAPIView):
queryset = StockItemTracking.objects.all() queryset = StockItemTracking.objects.all()
serializer_class = StockTrackingSerializer serializer_class = StockTrackingSerializer
permission_classes = [permissions.IsAuthenticated]
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
try: try:
@ -871,7 +861,6 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = StockLocation.objects.all() queryset = StockLocation.objects.all()
serializer_class = LocationSerializer serializer_class = LocationSerializer
permission_classes = (permissions.IsAuthenticated,)
stock_endpoints = [ stock_endpoints = [

View File

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

View File

@ -8,7 +8,7 @@
{% if location %} {% if location %}
<h3> <h3>
{{ location.name }} {{ 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> <a href="{% url 'admin:stock_stocklocation_change' location.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %} {% endif %}
</h3> </h3>

View File

@ -3,6 +3,8 @@ from rest_framework import status
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from InvenTree.helpers import addUserPermissions
from .models import StockLocation from .models import StockLocation
@ -22,6 +24,20 @@ class StockAPITestCase(APITestCase):
# Create a user for auth # Create a user for auth
User = get_user_model() User = get_user_model()
self.user = User.objects.create_user('testuser', 'test@testing.com', 'password') 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') self.client.login(username='testuser', password='password')
def doPost(self, url, data={}): 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" %} {% extends "base.html" %}
{% load i18n %}
{% block page_title %} {% block page_title %}
InvenTree | Index InvenTree | {% trans "Index" %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -9,24 +9,24 @@ InvenTree | Index
<hr> <hr>
<div class='col-sm-6'> <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/latest_parts.html" with collapse_id="latest_parts" %}
{% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %} {% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %}
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %} {% include "InvenTree/starred_parts.html" with collapse_id="starred" %}
{% endif %} {% endif %}
{% if perms.build.view_build %} {% if roles.build.view %}
{% include "InvenTree/build_pending.html" with collapse_id="build_pending" %} {% include "InvenTree/build_pending.html" with collapse_id="build_pending" %}
{% endif %} {% endif %}
</div> </div>
<div class='col-sm-6'> <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/low_stock.html" with collapse_id="order" %}
{% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %} {% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %}
{% endif %} {% endif %}
{% if perms.order.view_purchaseorder %} {% if roles.purchase_order.view %}
{% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %} {% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %}
{% endif %} {% endif %}
{% if perms.order.view_salesorder %} {% if roles.sales_order.view %}
{% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %} {% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %}
{% endif %} {% endif %}
</div> </div>

View File

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

View File

@ -1,3 +1,3 @@
<div> <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> </div>

View File

@ -6,19 +6,27 @@
<button class='btn btn-default' id='stock-export' title='{% trans "Export Stock Information" %}'>{% trans "Export" %}</button> <button class='btn btn-default' id='stock-export' title='{% trans "Export Stock Information" %}'>{% trans "Export" %}</button>
{% if read_only %} {% if read_only %}
{% else %} {% else %}
{% if roles.stock.add %}
<button class="btn btn-success" id='item-create'>{% trans "New Stock Item" %}</button> <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"> <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> <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"> <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-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-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-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-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> <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> <li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'>{% trans "Delete Stock" %}</a></li>
{% endif %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
{% endif %}
</div> </div>
<div class='filter-list' id='filter-list-stock'> <div class='filter-list' id='filter-list-stock'>
<!-- An empty div in which the filter list will be constructed --> <!-- An empty div in which the filter list will be constructed -->

View File

@ -119,15 +119,12 @@ class RoleGroupAdmin(admin.ModelAdmin):
# Save inlines before model # Save inlines before model
# https://stackoverflow.com/a/14860703/12794913 # https://stackoverflow.com/a/14860703/12794913
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
if obj is not None: pass # don't actually save the parent instance
# 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): def save_formset(self, request, form, formset, change):
formset.save() # this will save the children 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): class InvenTreeUserAdmin(UserAdmin):

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

@ -36,6 +36,10 @@ class RuleSet(models.Model):
choice[0] for choice in RULESET_CHOICES choice[0] for choice in RULESET_CHOICES
] ]
RULESET_PERMISSIONS = [
'view', 'add', 'change', 'delete',
]
RULESET_MODELS = { RULESET_MODELS = {
'admin': [ 'admin': [
'auth_group', '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_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')) can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items'))
@ -155,8 +159,15 @@ class RuleSet(models.Model):
model=model model=model
) )
def __str__(self): def __str__(self, debug=False):
return self.name """ 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): def save(self, *args, **kwargs):
@ -171,6 +182,10 @@ class RuleSet(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
if self.group:
# Update the group too!
self.group.save()
def get_models(self): def get_models(self):
""" """
Return the database tables / models that this ruleset covers. 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) 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

View File

@ -137,7 +137,8 @@ class RuleSetModelTest(TestCase):
rule.save() 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 # There should now be three permissions for each rule set
self.assertEqual(group.permissions.count(), 3 * len(permission_set)) self.assertEqual(group.permissions.count(), 3 * len(permission_set))
@ -151,7 +152,8 @@ class RuleSetModelTest(TestCase):
rule.save() 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 # There should now not be any permissions assigned to this group
self.assertEqual(group.permissions.count(), 0) self.assertEqual(group.permissions.count(), 0)