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,
},
'STOCK_OWNERSHIP_CONTROL': {
'name': _('Stock Ownership Control'),
'description': _('Enable ownership control over stock locations and items'),
'default': False,
'validator': bool,
},
'BUILDORDER_REFERENCE_PREFIX': {
'name': _('Build Order Reference Prefix'),
'description': _('Prefix value for build order reference'),

View File

@ -25,6 +25,18 @@
lft: 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'
- model: stock.stockitem
pk: 1234

View File

@ -90,7 +90,8 @@ class EditStockLocationForm(HelperForm):
fields = [
'name',
'parent',
'description'
'description',
'owner',
]
@ -138,6 +139,7 @@ class CreateStockItemForm(HelperForm):
'link',
'delete_on_deplete',
'status',
'owner',
]
# Custom clean to prevent complex StockItem.clean() logic from running (yet)
@ -414,6 +416,7 @@ class EditStockItemForm(HelperForm):
'purchase_price',
'link',
'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.functions import Coalesce
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.dispatch import receiver
@ -47,6 +47,10 @@ class StockLocation(InvenTreeTree):
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):
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'),
)
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):
"""
Returns True if this Stock item is "stale".

View File

@ -8,17 +8,22 @@
{% include "stock/tabs.html" with tab="tracking" %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
<h4>{% trans "Stock Tracking Information" %}</h4>
<hr>
{% if roles.stock.change %}
<div id='table-toolbar'>
<div class='btn-group'>
<button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'>
<span class='fas fa-plus-circle'></span> {% trans "New Entry" %}
</button>
<!-- 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 class='btn-group'>
<button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'>
<span class='fas fa-plus-circle'></span> {% trans "New Entry" %}
</button>
</div>
</div>
</div>
{% endif %}
{% endif %}
<table class='table table-condensed table-striped' id='track-table' data-toolbar='#table-toolbar'>
</table>

View File

@ -15,6 +15,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% block pre_content %}
{% include 'stock/loc_link.html' with location=item.location %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if item.is_building %}
<div class='alert alert-block alert-info'>
{% trans "This stock item is in production and cannot be edited." %}<br>
@ -29,6 +31,12 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
</div>
{% 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 %}
<div class='alert alert-block alert-danger'>
{% trans "This stock item has not passed all required tests" %}
@ -68,6 +76,9 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% endblock %}
{% block page_data %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
<h3>
{% trans "Stock Item" %}
{% if item.is_expired %}
@ -132,54 +143,57 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
</div>
{% endif %}
<!-- Stock adjustment menu -->
{% if roles.stock.change and not item.is_building %}
<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>
<ul class='dropdown-menu' role='menu'>
{% if item.in_stock %}
{% if not item.serialized %}
<li><a href='#' id='stock-count' title='{% trans "Count stock" %}'><span class='fas fa-clipboard-list'></span> {% trans "Count stock" %}</a></li>
<li><a href='#' id='stock-add' title='{% trans "Add stock" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
<li><a href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
{% endif %}
<li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
{% if item.part.trackable and not item.serialized %}
<li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
{% endif %}
{% endif %}
{% if item.part.salable and not item.customer %}
<li><a href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
{% endif %}
{% if item.customer %}
<li><a href='#' id='stock-return-from-customer' title='{% trans "Return to stock" %}'><span class='fas fa-undo'></span> {% trans "Return to stock" %}</a></li>
{% endif %}
{% if item.belongs_to %}
<li>
<a href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
<!-- Edit stock item -->
{% if roles.stock.change and not item.is_building %}
<div class='btn-group'>
<button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-tools'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.part.has_variants %}
<li><a href='#' id='stock-convert' title='{% trans "Convert to variant" %}'><span class='fas fa-screwdriver'></span> {% trans "Convert to variant" %}</a></li>
{% endif %}
{% if roles.stock.add %}
<li><a href='#' id='stock-duplicate' title='{% trans "Duplicate stock item" %}'><span class='fas fa-copy'></span> {% trans "Duplicate stock item" %}</a></li>
{% endif %}
<li><a href='#' id='stock-edit' title='{% trans "Edit stock item" %}'><span class='fas fa-edit icon-blue'></span> {% trans "Edit stock item" %}</a></li>
{% if user.is_staff or roles.stock.delete %}
{% if item.can_delete %}
<li><a href='#' id='stock-delete' title='{% trans "Delete stock item" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete stock item" %}</a></li>
<!-- 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 %}
<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>
<ul class='dropdown-menu' role='menu'>
{% if item.in_stock %}
{% if not item.serialized %}
<li><a href='#' id='stock-count' title='{% trans "Count stock" %}'><span class='fas fa-clipboard-list'></span> {% trans "Count stock" %}</a></li>
<li><a href='#' id='stock-add' title='{% trans "Add stock" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
<li><a href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
{% endif %}
{% endif %}
</ul>
</div>
<li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
{% if item.part.trackable and not item.serialized %}
<li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
{% endif %}
{% endif %}
{% if item.part.salable and not item.customer %}
<li><a href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
{% endif %}
{% if item.customer %}
<li><a href='#' id='stock-return-from-customer' title='{% trans "Return to stock" %}'><span class='fas fa-undo'></span> {% trans "Return to stock" %}</a></li>
{% endif %}
{% if item.belongs_to %}
<li>
<a href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
<!-- Edit stock item -->
{% if roles.stock.change and not item.is_building %}
<div class='btn-group'>
<button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-tools'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.part.has_variants %}
<li><a href='#' id='stock-convert' title='{% trans "Convert to variant" %}'><span class='fas fa-screwdriver'></span> {% trans "Convert to variant" %}</a></li>
{% endif %}
{% if roles.stock.add %}
<li><a href='#' id='stock-duplicate' title='{% trans "Duplicate stock item" %}'><span class='fas fa-copy'></span> {% trans "Duplicate stock item" %}</a></li>
{% endif %}
<li><a href='#' id='stock-edit' title='{% trans "Edit stock item" %}'><span class='fas fa-edit icon-blue'></span> {% trans "Edit stock item" %}</a></li>
{% if user.is_staff or roles.stock.delete %}
{% if item.can_delete %}
<li><a href='#' id='stock-delete' title='{% trans "Delete stock item" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete stock item" %}</a></li>
{% endif %}
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
</div>

View File

@ -1,8 +1,17 @@
{% extends "stock/stock_app_base.html" %}
{% load static %}
{% load inventree_extras %}
{% load i18n %}
{% 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='col-sm-6'>
{% if location %}
@ -18,11 +27,13 @@
<p>{% trans "All stock items" %}</p>
{% endif %}
<div class='btn-group action-buttons' role='group'>
{% if roles.stock.add %}
<button class='btn btn-default' id='location-create' title='{% trans "Create new stock location" %}'>
<span class='fas fa-plus-circle icon-green'/>
</button>
{% endif %}
{% 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 %}
<button class='btn btn-default' id='location-create' title='{% trans "Create new stock location" %}'>
<span class='fas fa-plus-circle icon-green'/>
</button>
{% endif %}
{% endif %}
<!-- Barcode actions menu -->
{% if location %}
<div class='btn-group'>
@ -33,23 +44,26 @@
<li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
</ul>
</div>
{% if roles.stock.change %}
<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>
<ul class='dropdown-menu' role='menu'>
<li><a href='#' id='location-count'><span class='fas fa-clipboard-list'></span>
{% trans "Count stock" %}</a></li>
</ul>
</div>
<div class='btn-group'>
<button id='location-actions' title='{% trans "Location actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle="dropdown"><span class='fas fa-sitemap'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
<li><a href='#' id='location-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit location" %}</a></li>
{% if roles.stock.delete %}
<li><a href='#' id='location-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete location" %}</a></li>
{% endif %}
</ul>
</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 %}
<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>
<ul class='dropdown-menu' role='menu'>
<li><a href='#' id='location-count'><span class='fas fa-clipboard-list'></span>
{% trans "Count stock" %}</a></li>
</ul>
</div>
<div class='btn-group'>
<button id='location-actions' title='{% trans "Location actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle="dropdown"><span class='fas fa-sitemap'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
<li><a href='#' id='location-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit location" %}</a></li>
{% if roles.stock.delete %}
<li><a href='#' id='location-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete location" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
{% endif %}
</div>

View File

@ -10,6 +10,9 @@ from common.models import InvenTreeSetting
import json
from datetime import datetime, timedelta
from common.models import InvenTreeSetting
from InvenTree.status_codes import StockStatus
class StockViewTestCase(TestCase):
@ -230,3 +233,163 @@ class StockItemTest(StockViewTestCase):
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
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 import HiddenInput
from django.urls import reverse
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
@ -35,6 +36,7 @@ from label.models import StockItemLabel
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
import common.settings
from common.models import InvenTreeSetting
from .admin import StockItemResource
@ -127,6 +129,7 @@ class StockLocationEdit(AjaxUpdateView):
""" Customize form data for StockLocation editing.
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()
@ -139,8 +142,33 @@ class StockLocationEdit(AjaxUpdateView):
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
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):
""" 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:
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
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):
"""
@ -1376,6 +1427,72 @@ class StockLocationCreate(AjaxCreateView):
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):
""" View for manually serializing a StockItem """
@ -1570,7 +1687,29 @@ class StockItemCreate(AjaxCreateView):
# Otherwise if the user has selected a SupplierPart, we know what Part they meant!
if form['supplier_part'].value() is not None:
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
def get_initial(self):
@ -1647,10 +1786,15 @@ class StockItemCreate(AjaxCreateView):
data = form.cleaned_data
part = data['part']
part = data.get('part', None)
quantity = data.get('quantity', None)
owner = data.get('owner', None)
if not part:
return
if not quantity:
return
@ -1681,6 +1825,15 @@ class StockItemCreate(AjaxCreateView):
_('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):
"""
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_ALLOW_EXPIRED_SALE" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" %}
{% include "InvenTree/settings/setting.html" with key="STOCK_OWNERSHIP_CONTROL" %}
</tbody>
</table>
{% endblock %}
{% endblock %}

View File

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

View File

@ -1,4 +1,7 @@
{% load i18n %}
{% load inventree_extras %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
<div id='button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
@ -8,28 +11,31 @@
</button>
{% if read_only %}
{% else %}
{% if roles.stock.add %}
<button class="btn btn-success" id='item-create'>
<span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %}
</button>
{% endif %}
{% if roles.stock.change or roles.stock.delete %}
<div class="btn-group">
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
<ul class="dropdown-menu">
{% if roles.stock.change %}
<li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle'></span> {% trans "Add stock" %}</a></li>
<li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li>
<li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li>
<li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li>
<li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
<!-- 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 %}
<button class="btn btn-success" id='item-create'>
<span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %}
</button>
{% endif %}
{% if roles.stock.delete %}
<li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt'></span> {% trans "Delete Stock" %}</a></li>
{% if roles.stock.change or roles.stock.delete %}
<div class="btn-group">
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
<ul class="dropdown-menu">
{% if roles.stock.change %}
<li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle'></span> {% trans "Add stock" %}</a></li>
<li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li>
<li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li>
<li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li>
<li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
{% endif %}
{% if roles.stock.delete %}
<li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt'></span> {% trans "Delete Stock" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
{% endif %}
</div>
<div class='filter-list' id='filter-list-stock'>