mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'stock_owner' of github.com:eeintech/InvenTree into stock_owner
This commit is contained in:
commit
d25a719724
@ -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'),
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
27
InvenTree/stock/migrations/0057_auto_20210107_1904.py
Normal file
27
InvenTree/stock/migrations/0057_auto_20210107_1904.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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".
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
@ -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'>
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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 %}
|
@ -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'>
|
||||||
|
@ -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 -->
|
||||||
|
Loading…
Reference in New Issue
Block a user