Merge branch 'stock_owner' of github.com:eeintech/InvenTree into stock_owner

This commit is contained in:
eeintech 2021-01-08 13:51:49 -05:00
commit d25a719724
13 changed files with 514 additions and 103 deletions

View File

@ -189,6 +189,13 @@ class InvenTreeSetting(models.Model):
'validator': bool, 'validator': bool,
}, },
'STOCK_OWNERSHIP_CONTROL': {
'name': _('Stock Ownership Control'),
'description': _('Enable ownership control over stock locations and items'),
'default': False,
'validator': bool,
},
'BUILDORDER_REFERENCE_PREFIX': { 'BUILDORDER_REFERENCE_PREFIX': {
'name': _('Build Order Reference Prefix'), 'name': _('Build Order Reference Prefix'),
'description': _('Prefix value for build order reference'), 'description': _('Prefix value for build order reference'),

View File

@ -25,6 +25,18 @@
lft: 0 lft: 0
rght: 0 rght: 0
# Capacitor C_22N_0805 in 'Office'
- model: stock.stockitem
pk: 11
fields:
part: 5
location: 4
quantity: 666
level: 0
tree_id: 0
lft: 0
rght: 0
# 1234 2K2 resistors in 'Drawer_1' # 1234 2K2 resistors in 'Drawer_1'
- model: stock.stockitem - model: stock.stockitem
pk: 1234 pk: 1234

View File

@ -90,7 +90,8 @@ class EditStockLocationForm(HelperForm):
fields = [ fields = [
'name', 'name',
'parent', 'parent',
'description' 'description',
'owner',
] ]
@ -138,6 +139,7 @@ class CreateStockItemForm(HelperForm):
'link', 'link',
'delete_on_deplete', 'delete_on_deplete',
'status', 'status',
'owner',
] ]
# Custom clean to prevent complex StockItem.clean() logic from running (yet) # Custom clean to prevent complex StockItem.clean() logic from running (yet)
@ -414,6 +416,7 @@ class EditStockItemForm(HelperForm):
'purchase_price', 'purchase_price',
'link', 'link',
'delete_on_deplete', 'delete_on_deplete',
'owner',
] ]

View File

@ -0,0 +1,27 @@
# Generated by Django 3.0.7 on 2021-01-07 19:04
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('auth', '0011_update_proxy_permissions'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('stock', '0056_stockitem_expiry_date'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='owner',
field=models.ForeignKey(blank=True, help_text='Owner (User)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner_stockitems', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='stocklocation',
name='owner',
field=models.ForeignKey(blank=True, help_text='Owner (Group)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner_stocklocations', to='auth.Group'),
),
]

View File

@ -16,7 +16,7 @@ from django.db import models, transaction
from django.db.models import Sum, Q from django.db.models import Sum, Q
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.contrib.auth.models import User from django.contrib.auth.models import User, Group
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
@ -47,6 +47,10 @@ class StockLocation(InvenTreeTree):
Stock locations can be heirarchical as required Stock locations can be heirarchical as required
""" """
owner = models.ForeignKey(Group, on_delete=models.SET_NULL, blank=True, null=True,
help_text='Owner (Group)',
related_name='owner_stocklocations')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('stock-location-detail', kwargs={'pk': self.id}) return reverse('stock-location-detail', kwargs={'pk': self.id})
@ -474,6 +478,10 @@ class StockItem(MPTTModel):
help_text=_('Single unit purchase price at time of purchase'), help_text=_('Single unit purchase price at time of purchase'),
) )
owner = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True,
help_text='Owner (User)',
related_name='owner_stockitems')
def is_stale(self): def is_stale(self):
""" """
Returns True if this Stock item is "stale". Returns True if this Stock item is "stale".

View File

@ -8,10 +8,14 @@
{% include "stock/tabs.html" with tab="tracking" %} {% include "stock/tabs.html" with tab="tracking" %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
<h4>{% trans "Stock Tracking Information" %}</h4> <h4>{% trans "Stock Tracking Information" %}</h4>
<hr> <hr>
{% if roles.stock.change %} <!-- Check permissions and owner -->
{% if owner_control.value == "False" or owner_control.value == "True" and item.owner == user %}
{% if roles.stock.change and not item.is_building %}
<div id='table-toolbar'> <div id='table-toolbar'>
<div class='btn-group'> <div class='btn-group'>
<button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'> <button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'>
@ -20,6 +24,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endif %}
<table class='table table-condensed table-striped' id='track-table' data-toolbar='#table-toolbar'> <table class='table table-condensed table-striped' id='track-table' data-toolbar='#table-toolbar'>
</table> </table>

View File

@ -15,6 +15,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% block pre_content %} {% block pre_content %}
{% include 'stock/loc_link.html' with location=item.location %} {% include 'stock/loc_link.html' with location=item.location %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if item.is_building %} {% if item.is_building %}
<div class='alert alert-block alert-info'> <div class='alert alert-block alert-info'>
{% trans "This stock item is in production and cannot be edited." %}<br> {% trans "This stock item is in production and cannot be edited." %}<br>
@ -29,6 +31,12 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
</div> </div>
{% endif %} {% endif %}
{% if owner_control.value == "True" and not item.owner == user and not user.is_superuser %}
<div class='alert alert-block alert-info'>
{% trans "You are not the owner of this item. This stock item cannot be edited." %}<br>
</div>
{% endif %}
{% if item.hasRequiredTests and not item.passedAllRequiredTests %} {% if item.hasRequiredTests and not item.passedAllRequiredTests %}
<div class='alert alert-block alert-danger'> <div class='alert alert-block alert-danger'>
{% trans "This stock item has not passed all required tests" %} {% trans "This stock item has not passed all required tests" %}
@ -68,6 +76,9 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% endblock %} {% endblock %}
{% block page_data %} {% block page_data %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
<h3> <h3>
{% trans "Stock Item" %} {% trans "Stock Item" %}
{% if item.is_expired %} {% if item.is_expired %}
@ -132,6 +143,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
</div> </div>
{% endif %} {% endif %}
<!-- Stock adjustment menu --> <!-- Stock adjustment menu -->
<!-- Check permissions and owner -->
{% if owner_control.value == "False" or owner_control.value == "True" and item.owner == user or user.is_superuser %}
{% if roles.stock.change and not item.is_building %} {% if roles.stock.change and not item.is_building %}
<div class='btn-group'> <div class='btn-group'>
<button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button> <button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
@ -181,6 +194,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,8 +1,17 @@
{% extends "stock/stock_app_base.html" %} {% extends "stock/stock_app_base.html" %}
{% load static %} {% load static %}
{% load inventree_extras %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if location and owner_control.value == "True" and not location.owner in user.groups.all and not user.is_superuser %}
<div class='alert alert-block alert-info'>
{% trans "You are not in the list of owners of this location. This stock location cannot be edited." %}<br>
</div>
{% endif %}
<div class='row'> <div class='row'>
<div class='col-sm-6'> <div class='col-sm-6'>
{% if location %} {% if location %}
@ -18,11 +27,13 @@
<p>{% trans "All stock items" %}</p> <p>{% trans "All stock items" %}</p>
{% endif %} {% endif %}
<div class='btn-group action-buttons' role='group'> <div class='btn-group action-buttons' role='group'>
{% if owner_control.value == "False" or owner_control.value == "True" and location.owner in user.groups.all or user.is_superuser or not location %}
{% if roles.stock.add %} {% if roles.stock.add %}
<button class='btn btn-default' id='location-create' title='{% trans "Create new stock location" %}'> <button class='btn btn-default' id='location-create' title='{% trans "Create new stock location" %}'>
<span class='fas fa-plus-circle icon-green'/> <span class='fas fa-plus-circle icon-green'/>
</button> </button>
{% endif %} {% endif %}
{% endif %}
<!-- Barcode actions menu --> <!-- Barcode actions menu -->
{% if location %} {% if location %}
<div class='btn-group'> <div class='btn-group'>
@ -33,6 +44,8 @@
<li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li> <li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
</ul> </ul>
</div> </div>
<!-- Check permissions and owner -->
{% if owner_control.value == "False" or owner_control.value == "True" and location.owner in user.groups.all or user.is_superuser %}
{% if roles.stock.change %} {% if roles.stock.change %}
<div class='btn-group'> <div class='btn-group'>
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button> <button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
@ -52,6 +65,7 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
</div> </div>
</div> </div>
<div class='col-sm-6'> <div class='col-sm-6'>

View File

@ -10,6 +10,9 @@ from common.models import InvenTreeSetting
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from common.models import InvenTreeSetting
from InvenTree.status_codes import StockStatus
class StockViewTestCase(TestCase): class StockViewTestCase(TestCase):
@ -230,3 +233,163 @@ class StockItemTest(StockViewTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = json.loads(response.content) data = json.loads(response.content)
self.assertFalse(data['form_valid']) self.assertFalse(data['form_valid'])
class StockOwnershipTest(StockViewTestCase):
""" Tests for stock ownership views """
def setUp(self):
""" Add another user for ownership tests """
super().setUp()
# Promote existing user with staff, admin and superuser statuses
self.user.is_staff = True
self.user.is_admin = True
self.user.is_superuser = True
self.user.save()
# Create a new user
user = get_user_model()
self.new_user = user.objects.create_user(
username='john',
email='john@email.com',
password='custom123',
)
# Put the user into a new group with the correct permissions
group = Group.objects.create(name='new_group')
self.new_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()
def enable_ownership(self):
# Enable stock location ownership
InvenTreeSetting.set_setting('STOCK_OWNERSHIP_CONTROL', True, self.user)
self.assertEqual(True, InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL'))
def test_owner_control(self):
# Test stock location and item ownership
from .models import StockLocation, StockItem
user_group = self.user.groups.all()[0]
new_user_group = self.new_user.groups.all()[0]
test_location_id = 4
test_item_id = 11
# Enable ownership control
self.enable_ownership()
# Set ownership on existing location
response = self.client.post(reverse('stock-location-edit', args=(test_location_id,)),
{'name': 'Office', 'owner': user_group.pk},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": true', status_code=200)
# Set ownership on existing item (and change location)
response = self.client.post(reverse('stock-item-edit', args=(test_item_id,)),
{'part': 1, 'status': StockStatus.OK, 'owner': self.user.pk},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": true', status_code=200)
# Logout
self.client.logout()
# Login with new user
self.client.login(username='john', password='custom123')
# Test location edit
response = self.client.post(reverse('stock-location-edit', args=(test_location_id,)),
{'name': 'Office', 'owner': new_user_group.pk},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Make sure the location's owner is unchanged
location = StockLocation.objects.get(pk=test_location_id)
self.assertEqual(location.owner, user_group)
# Test item edit
response = self.client.post(reverse('stock-item-edit', args=(test_item_id,)),
{'part': 1, 'status': StockStatus.OK, 'owner': self.new_user.pk},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": false', status_code=200)
# Make sure the item's owner is unchanged
item = StockItem.objects.get(pk=test_item_id)
self.assertEqual(item.owner, self.user)
# Create new parent location
parent_location = {
'name': 'John Desk',
'description': 'John\'s desk',
'owner': new_user_group.pk,
}
# Create new parent location
response = self.client.post(reverse('stock-location-create'),
parent_location, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": true', status_code=200)
# Retrieve created location
parent_location = StockLocation.objects.get(name=parent_location['name'])
# Create new child location
new_location = {
'name': 'Upper Left Drawer',
'description': 'John\'s desk - Upper left drawer',
}
# Try to create new location with neither parent or owner
response = self.client.post(reverse('stock-location-create'),
new_location, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": false', status_code=200)
# Try to create new location with invalid owner
new_location['parent'] = parent_location.id
new_location['owner'] = user_group.pk
response = self.client.post(reverse('stock-location-create'),
new_location, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": false', status_code=200)
# Try to create new location with valid owner
new_location['owner'] = new_user_group.pk
response = self.client.post(reverse('stock-location-create'),
new_location, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": true', status_code=200)
# Retrieve created location
location_created = StockLocation.objects.get(name=new_location['name'])
# Create new item
new_item = {
'part': 25,
'location': location_created.pk,
'quantity': 123,
'status': StockStatus.OK,
}
# Try to create new item with no owner
response = self.client.post(reverse('stock-item-create'),
new_item, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": false', status_code=200)
# Try to create new item with invalid owner
new_item['owner'] = self.user.pk
response = self.client.post(reverse('stock-item-create'),
new_item, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": false', status_code=200)
# Try to create new item with valid owner
new_item['owner'] = self.new_user.pk
response = self.client.post(reverse('stock-item-create'),
new_item, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": true', status_code=200)

View File

@ -11,6 +11,7 @@ from django.views.generic import DetailView, ListView, UpdateView
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from django.forms import HiddenInput from django.forms import HiddenInput
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -35,6 +36,7 @@ from label.models import StockItemLabel
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
import common.settings import common.settings
from common.models import InvenTreeSetting
from .admin import StockItemResource from .admin import StockItemResource
@ -127,6 +129,7 @@ class StockLocationEdit(AjaxUpdateView):
""" Customize form data for StockLocation editing. """ Customize form data for StockLocation editing.
Limit the choices for 'parent' field to those which make sense. Limit the choices for 'parent' field to those which make sense.
If ownership control is enabled and location has parent, disable owner field.
""" """
form = super(AjaxUpdateView, self).get_form() form = super(AjaxUpdateView, self).get_form()
@ -139,8 +142,33 @@ class StockLocationEdit(AjaxUpdateView):
form.fields['parent'].queryset = parent_choices form.fields['parent'].queryset = parent_choices
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if not stock_ownership_control:
form.fields['owner'].widget = HiddenInput()
else:
if location.parent:
form.fields['owner'].initial = location.parent.owner
if not self.request.user.is_superuser:
form.fields['owner'].disabled = True
return form return form
def save(self, object, form, **kwargs):
""" If location has children and ownership control is enabled:
- update all children's owners with location's owner
"""
self.object = form.save()
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if self.object.get_children() and stock_ownership_control:
for child in self.object.get_children():
child.owner = self.object.owner
child.save()
return self.object
class StockLocationQRCode(QRCodeView): class StockLocationQRCode(QRCodeView):
""" View for displaying a QR code for a StockLocation object """ """ View for displaying a QR code for a StockLocation object """
@ -1322,8 +1350,31 @@ class StockItemEdit(AjaxUpdateView):
if not item.part.trackable and not item.serialized: if not item.part.trackable and not item.serialized:
form.fields['serial'].widget = HiddenInput() form.fields['serial'].widget = HiddenInput()
location = item.location
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if not stock_ownership_control:
form.fields['owner'].widget = HiddenInput()
else:
if location:
# Check if location has owner
if location.owner:
form.fields['owner'].queryset = User.objects.filter(groups=location.owner)
return form return form
def validate(self, item, form):
""" Check that owner is set if stock ownership control is enabled """
owner = form.cleaned_data.get('owner', None)
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if not owner and stock_ownership_control:
form.add_error('owner', _('Owner is required (ownership control is enabled)'))
class StockItemConvert(AjaxUpdateView): class StockItemConvert(AjaxUpdateView):
""" """
@ -1376,6 +1427,72 @@ class StockLocationCreate(AjaxCreateView):
return initials return initials
def get_form(self):
""" Disable owner field when:
- creating child location
- and stock ownership control is enable
"""
form = super().get_form()
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if not stock_ownership_control:
form.fields['owner'].widget = HiddenInput()
else:
# If user did not selected owner, automatically match to parent's owner
if not form['owner'].data:
try:
parent_id = form['parent'].value()
parent = StockLocation.objects.get(pk=parent_id)
if parent:
form.fields['owner'].initial = parent.owner
if not self.request.user.is_superuser:
form.fields['owner'].disabled = True
except StockLocation.DoesNotExist:
pass
except ValueError:
pass
return form
def save(self, form):
""" If parent location exists then use it to set the owner """
self.object = form.save(commit=False)
parent = form.cleaned_data.get('parent', None)
if parent:
self.object.owner = parent.owner
self.object.save()
return self.object
def validate(self, item, form):
""" Check that owner is set if stock ownership control is enabled """
parent = form.cleaned_data.get('parent', None)
owner = form.cleaned_data.get('owner', None)
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if stock_ownership_control:
if not owner:
form.add_error('owner', _('Owner is required (ownership control is enabled)'))
else:
try:
if parent.owner:
if parent.owner != owner:
error = f'Owner requires to be equivalent to parent\'s owner ({parent.owner})'
form.add_error('owner', error)
except AttributeError:
# No parent
pass
class StockItemSerialize(AjaxUpdateView): class StockItemSerialize(AjaxUpdateView):
""" View for manually serializing a StockItem """ """ View for manually serializing a StockItem """
@ -1571,6 +1688,28 @@ class StockItemCreate(AjaxCreateView):
if form['supplier_part'].value() is not None: if form['supplier_part'].value() is not None:
pass pass
location = None
try:
loc_id = form['location'].value()
location = StockLocation.objects.get(pk=loc_id)
except StockLocation.DoesNotExist:
pass
except ValueError:
pass
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if not stock_ownership_control:
form.fields['owner'].widget = HiddenInput()
else:
if location:
# Check if location has owner
if location.owner:
queryset = User.objects.filter(groups=location.owner)
if self.request.user in queryset:
form.fields['owner'].initial = self.request.user
form.fields['owner'].queryset = queryset
return form return form
def get_initial(self): def get_initial(self):
@ -1647,10 +1786,15 @@ class StockItemCreate(AjaxCreateView):
data = form.cleaned_data data = form.cleaned_data
part = data['part'] part = data.get('part', None)
quantity = data.get('quantity', None) quantity = data.get('quantity', None)
owner = data.get('owner', None)
if not part:
return
if not quantity: if not quantity:
return return
@ -1681,6 +1825,15 @@ class StockItemCreate(AjaxCreateView):
_('Serial numbers already exist') + ': ' + exists _('Serial numbers already exist') + ': ' + exists
) )
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if stock_ownership_control:
# Check if owner is set
if not owner:
form.add_error('owner', _('Owner is required (ownership control is enabled)'))
return
def save(self, form, **kwargs): def save(self, form, **kwargs):
""" """
Create a new StockItem based on the provided form data. Create a new StockItem based on the provided form data.

View File

@ -19,6 +19,7 @@
{% include "InvenTree/settings/setting.html" with key="STOCK_STALE_DAYS" %} {% include "InvenTree/settings/setting.html" with key="STOCK_STALE_DAYS" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_OWNERSHIP_CONTROL" %}
</tbody> </tbody>
</table> </table>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,5 @@
{% load i18n %} {% load i18n %}
{% if roles.stock.change %}
<div id='attachment-buttons'> <div id='attachment-buttons'>
<div class='btn-group'> <div class='btn-group'>
<button type='button' class='btn btn-success' id='new-attachment'> <button type='button' class='btn btn-success' id='new-attachment'>
@ -8,7 +7,6 @@
</button> </button>
</div> </div>
</div> </div>
{% endif %}
<div class='dropzone' id='attachment-dropzone'> <div class='dropzone' id='attachment-dropzone'>
<table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'> <table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'>

View File

@ -1,4 +1,7 @@
{% load i18n %} {% load i18n %}
{% load inventree_extras %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
<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;'>
@ -8,6 +11,8 @@
</button> </button>
{% if read_only %} {% if read_only %}
{% else %} {% else %}
<!-- Check permissions and owner -->
{% if owner_control.value == "False" or owner_control.value == "True" and location.owner in user.groups.all or user.is_superuser %}
{% if roles.stock.add %} {% if roles.stock.add %}
<button class="btn btn-success" id='item-create'> <button class="btn btn-success" id='item-create'>
<span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %} <span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %}
@ -31,6 +36,7 @@
</div> </div>
{% endif %} {% endif %}
{% 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 -->