2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

Merge remote-tracking branch 'inventree/master' into batch-reports

# Conflicts:
#	InvenTree/templates/stock_table.html
This commit is contained in:
Oliver Walters
2021-01-18 23:23:58 +11:00
28 changed files with 2265 additions and 1235 deletions

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,25 @@
# Generated by Django 3.0.7 on 2021-01-11 21:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0005_owner_model'),
('stock', '0056_stockitem_expiry_date'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='owner',
field=models.ForeignKey(blank=True, help_text='Select Owner', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='users.Owner'),
),
migrations.AddField(
model_name='stocklocation',
name='owner',
field=models.ForeignKey(blank=True, help_text='Select Owner', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_locations', to='users.Owner'),
),
]

View File

@ -38,6 +38,8 @@ from InvenTree.status_codes import StockStatus
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeURLField
from users.models import Owner
from company import models as CompanyModels
from part import models as PartModels
@ -48,6 +50,10 @@ class StockLocation(InvenTreeTree):
Stock locations can be heirarchical as required
"""
owner = models.ForeignKey(Owner, on_delete=models.SET_NULL, blank=True, null=True,
help_text='Select Owner',
related_name='stock_locations')
def get_absolute_url(self):
return reverse('stock-location-detail', kwargs={'pk': self.id})
@ -489,6 +495,10 @@ class StockItem(MPTTModel):
help_text=_('Single unit purchase price at time of purchase'),
)
owner = models.ForeignKey(Owner, on_delete=models.SET_NULL, blank=True, null=True,
help_text='Select Owner',
related_name='stock_items')
def is_stale(self):
"""
Returns True if this Stock item is "stale".

View File

@ -8,17 +8,25 @@
{% include "stock/tabs.html" with tab="tracking" %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if owner_control.value == "True" %}
{% authorized_owners item.owner as owners %}
{% endif %}
<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 user in owners %}
{% 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,17 @@ 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 owner_control.value == "True" %}
{% authorized_owners item.owner as owners %}
{% if not user in owners and not user.is_superuser %}
<div class='alert alert-block alert-info'>
{% trans "You are not in the list of owners of this item. This stock item cannot be edited." %}<br>
</div>
{% endif %}
{% endif %}
{% if item.is_building %}
<div class='alert alert-block alert-info'>
{% trans "This stock item is in production and cannot be edited." %}<br>
@ -68,6 +79,12 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% endblock %}
{% block page_data %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if owner_control.value == "True" %}
{% authorized_owners item.owner as owners %}
{% endif %}
<h3>
{% trans "Stock Item" %}
{% if item.is_expired %}
@ -132,54 +149,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 user in owners 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,20 @@
{% extends "stock/stock_app_base.html" %}
{% load static %}
{% load inventree_extras %}
{% load i18n %}
{% block content %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if owner_control.value == "True" %}
{% authorized_owners location.owner as owners %}
{% if location and not user in owners 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 %}
{% endif %}
<div class='row'>
<div class='col-sm-6'>
{% if location %}
@ -18,11 +30,13 @@
<p>{% trans "All stock items" %}</p>
{% endif %}
<div class='btn-group action-buttons' role='group'>
{% if roles.stock_location.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 user in owners or user.is_superuser or not location %}
{% if roles.stock_location.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,25 +47,28 @@
<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>
{% endif %}
{% if roles.stock_location.change %}
<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_location.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 user in owners 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>
{% endif %}
{% if roles.stock_location.change %}
<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

@ -117,7 +117,7 @@ class StockItemListTest(StockAPITestCase):
response = self.get_stock()
self.assertEqual(len(response), 19)
self.assertEqual(len(response), 20)
def test_filter_by_part(self):
"""
@ -126,7 +126,7 @@ class StockItemListTest(StockAPITestCase):
response = self.get_stock(part=25)
self.assertEqual(len(response), 7)
self.assertEqual(len(response), 8)
response = self.get_stock(part=10004)
@ -166,7 +166,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 1)
response = self.get_stock(depleted=0)
self.assertEqual(len(response), 18)
self.assertEqual(len(response), 19)
def test_filter_by_in_stock(self):
"""
@ -174,7 +174,7 @@ class StockItemListTest(StockAPITestCase):
"""
response = self.get_stock(in_stock=1)
self.assertEqual(len(response), 16)
self.assertEqual(len(response), 17)
response = self.get_stock(in_stock=0)
self.assertEqual(len(response), 3)
@ -185,7 +185,7 @@ class StockItemListTest(StockAPITestCase):
"""
codes = {
StockStatus.OK: 17,
StockStatus.OK: 18,
StockStatus.DESTROYED: 1,
StockStatus.LOST: 1,
StockStatus.DAMAGED: 0,
@ -218,7 +218,7 @@ class StockItemListTest(StockAPITestCase):
self.assertIsNotNone(item['serial'])
response = self.get_stock(serialized=0)
self.assertEqual(len(response), 7)
self.assertEqual(len(response), 8)
for item in response:
self.assertIsNone(item['serial'])
@ -230,7 +230,7 @@ class StockItemListTest(StockAPITestCase):
# First, we can assume that the 'stock expiry' feature is disabled
response = self.get_stock(expired=1)
self.assertEqual(len(response), 19)
self.assertEqual(len(response), 20)
# Now, ensure that the expiry date feature is enabled!
InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user)
@ -242,7 +242,7 @@ class StockItemListTest(StockAPITestCase):
self.assertTrue(item['expired'])
response = self.get_stock(expired=0)
self.assertEqual(len(response), 18)
self.assertEqual(len(response), 19)
for item in response:
self.assertFalse(item['expired'])
@ -259,7 +259,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 4)
response = self.get_stock(expired=0)
self.assertEqual(len(response), 15)
self.assertEqual(len(response), 16)
class StockItemTest(StockAPITestCase):

View File

@ -10,6 +10,8 @@ from common.models import InvenTreeSetting
import json
from datetime import datetime, timedelta
from InvenTree.status_codes import StockStatus
class StockViewTestCase(TestCase):
@ -230,3 +232,184 @@ 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
from users.models import Owner
user_group = self.user.groups.all()[0]
user_group_owner = Owner.get_owner(user_group)
new_user_group = self.new_user.groups.all()[0]
new_user_group_owner = Owner.get_owner(new_user_group)
user_as_owner = Owner.get_owner(self.user)
new_user_as_owner = Owner.get_owner(self.new_user)
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_owner.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': user_as_owner.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_owner.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_owner)
# Test item edit
response = self.client.post(reverse('stock-item-edit', args=(test_item_id,)),
{'part': 1, 'status': StockStatus.OK, 'owner': new_user_as_owner.pk},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Make sure the item's owner is unchanged
item = StockItem.objects.get(pk=test_item_id)
self.assertEqual(item.owner, user_as_owner)
# Create new parent location
parent_location = {
'name': 'John Desk',
'description': 'John\'s desk',
'owner': new_user_group_owner.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_owner.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_owner.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'] = user_as_owner.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'] = new_user_as_owner.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)
# Logout
self.client.logout()
# Login with admin
self.client.login(username='username', password='password')
# Switch owner of location
response = self.client.post(reverse('stock-location-edit', args=(location_created.pk,)),
{'name': new_location['name'], 'owner': user_group_owner.pk},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": true', status_code=200)
# Check that owner was updated for item in this location
stock_item = StockItem.objects.all().last()
self.assertEqual(stock_item.owner, user_group_owner)

View File

@ -11,6 +11,8 @@ 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 import get_user_model
from django.contrib.auth.models import Group
from django.utils.translation import ugettext as _
@ -33,6 +35,8 @@ from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
import common.settings
from common.models import InvenTreeSetting
from users.models import Owner
from .admin import StockItemResource
@ -125,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()
@ -137,8 +142,105 @@ 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:
# Hide owner field
form.fields['owner'].widget = HiddenInput()
else:
# Get location's owner
location_owner = location.owner
if location_owner:
if location.parent:
try:
# If location has parent and owner: automatically select parent's owner
parent_owner = location.parent.owner
form.fields['owner'].initial = parent_owner
except AttributeError:
pass
else:
# If current owner exists: automatically select it
form.fields['owner'].initial = location_owner
# Update queryset or disable field (only if not admin)
if not self.request.user.is_superuser:
if type(location_owner.owner) is Group:
user_as_owner = Owner.get_owner(self.request.user)
queryset = location_owner.get_related_owners(include_group=True)
if user_as_owner not in queryset:
# Only owners or admin can change current owner
form.fields['owner'].disabled = True
else:
form.fields['owner'].queryset = queryset
return form
def save(self, object, form, **kwargs):
""" If location has children and ownership control is enabled:
- update owner of all children location of this location
- update owner for all stock items at this location
"""
self.object = form.save()
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if stock_ownership_control:
# Get authorized users
authorized_owners = self.object.owner.get_related_owners()
# Update children locations
children_locations = self.object.get_children()
for child in children_locations:
# Check if current owner is subset of new owner
if child.owner and authorized_owners:
if child.owner in authorized_owners:
continue
child.owner = self.object.owner
child.save()
# Update stock items
stock_items = self.object.get_stock_items()
for stock_item in stock_items:
# Check if current owner is subset of new owner
if stock_item.owner and authorized_owners:
if stock_item.owner in authorized_owners:
continue
stock_item.owner = self.object.owner
stock_item.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 and not self.request.user.is_superuser:
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 StockLocationQRCode(QRCodeView):
""" View for displaying a QR code for a StockLocation object """
@ -1082,6 +1184,18 @@ class StockAdjust(AjaxView, FormMixin):
count += 1
# Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
if stock_ownership_control:
# Fetch destination owner
destination_owner = destination.owner
if destination_owner:
# Update owner
item.owner = destination_owner
item.save()
if count == 0:
return _('No items were moved')
@ -1148,8 +1262,76 @@ 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:
try:
location_owner = location.owner
except AttributeError:
location_owner = None
# Check if location has owner
if location_owner:
form.fields['owner'].initial = location_owner
# Check location's owner type and filter potential owners
if type(location_owner.owner) is Group:
user_as_owner = Owner.get_owner(self.request.user)
queryset = location_owner.get_related_owners(include_group=True)
if user_as_owner in queryset:
form.fields['owner'].initial = user_as_owner
form.fields['owner'].queryset = queryset
elif type(location_owner.owner) is get_user_model():
# If location's owner is a user: automatically set owner field and disable it
form.fields['owner'].disabled = True
form.fields['owner'].initial = location_owner
try:
item_owner = item.owner
except AttributeError:
item_owner = None
# Check if item has owner
if item_owner:
form.fields['owner'].initial = item_owner
# Check item's owner type and filter potential owners
if type(item_owner.owner) is Group:
user_as_owner = Owner.get_owner(self.request.user)
queryset = item_owner.get_related_owners(include_group=True)
if user_as_owner in queryset:
form.fields['owner'].initial = user_as_owner
form.fields['owner'].queryset = queryset
elif type(item_owner.owner) is get_user_model():
# If item's owner is a user: automatically set owner field and disable it
form.fields['owner'].disabled = True
form.fields['owner'].initial = item_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 stock_ownership_control:
if not owner and not self.request.user.is_superuser:
form.add_error('owner', _('Owner is required (ownership control is enabled)'))
class StockItemConvert(AjaxUpdateView):
"""
@ -1202,6 +1384,76 @@ 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:
# Hide owner field
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:
# Select parent's owner
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 and not self.request.user.is_superuser:
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 """
@ -1396,7 +1648,42 @@ 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:
try:
location_owner = location.owner
except AttributeError:
location_owner = None
if location_owner:
# Check location's owner type and filter potential owners
if type(location_owner.owner) is Group:
user_as_owner = Owner.get_owner(self.request.user)
queryset = location_owner.get_related_owners()
if user_as_owner in queryset:
form.fields['owner'].initial = user_as_owner
form.fields['owner'].queryset = queryset
elif type(location_owner.owner) is get_user_model():
# If location's owner is a user: automatically set owner field and disable it
form.fields['owner'].disabled = True
form.fields['owner'].initial = location_owner
return form
def get_initial(self):
@ -1473,10 +1760,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
@ -1512,6 +1804,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 and not self.request.user.is_superuser:
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.