diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 97fc4fe69d..7659981ecd 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -10,7 +10,6 @@ from django.forms.utils import ErrorDict from django.utils.translation import ugettext as _ from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError -from django.contrib.auth.models import User, Group from mptt.fields import TreeNodeChoiceField @@ -86,37 +85,15 @@ class EditStockItemTestResultForm(HelperForm): class EditStockLocationForm(HelperForm): """ Form for editing a StockLocation """ - owner = forms.ChoiceField( - label=_('Owner'), - help_text=_('Select Owner') - ) - class Meta: model = StockLocation fields = [ 'name', 'parent', 'description', + 'owner', ] - def get_owner_choices(self): - - choices = [('', '-' * 10)] - - for group in Group.objects.all(): - choices.append((f'group_{group.name}', f'{group} (Group)')) - - for user in User.objects.all(): - choices.append((f'user_{user.username}', f'{user} (User)')) - - return choices - - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - self.fields['owner'].choices = self.get_owner_choices() - class ConvertStockItemForm(HelperForm): """ diff --git a/InvenTree/stock/migrations/0057_stock_location_item_owner.py b/InvenTree/stock/migrations/0057_stock_location_item_owner.py new file mode 100644 index 0000000000..78857ea528 --- /dev/null +++ b/InvenTree/stock/migrations/0057_stock_location_item_owner.py @@ -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', '0004_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.CASCADE, related_name='stock_locations', to='users.Owner'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 596422ed9c..c62b5b7251 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -16,11 +16,9 @@ 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, Group +from django.contrib.auth.models import User from django.db.models.signals import pre_delete from django.dispatch import receiver -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType from markdownx.models import MarkdownxField @@ -39,6 +37,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 @@ -49,28 +49,9 @@ class StockLocation(InvenTreeTree): Stock locations can be heirarchical as required """ - owner_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) - owner_id = models.PositiveIntegerField(null=True, blank=True) - owner = GenericForeignKey('owner_type', 'owner_id') - - def save(self, *args, **kwargs): - """ Custom save method to process StockLocation owner """ - - # Extract owner - try: - owner = kwargs.pop('owner') - except KeyError: - owner = '' - - # Set the owner - if owner.startswith('group'): - group_name = owner.replace('group_', '') - self.owner = Group.objects.get(name=group_name) - elif owner.startswith('user'): - user_name = owner.replace('user_', '') - self.owner = User.objects.get(username=user_name) - - super(StockLocation, self).save(*args, **kwargs) + owner = models.ForeignKey(Owner, on_delete=models.CASCADE, 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}) @@ -499,9 +480,9 @@ 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') + 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): """ diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 2d9a417f39..7b3cedb5b4 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -11,7 +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, Group +from django.contrib.auth.models import Group, User from django.utils.translation import ugettext as _ @@ -125,24 +125,6 @@ class StockLocationEdit(AjaxUpdateView): ajax_form_title = _('Edit Stock Location') role_required = 'stock.change' - def get_owner_initial(self): - - initial = '' - - location = self.get_object() - - owner = location.owner - - if owner: - if type(owner) is Group: - group_name = owner.name - initial = f'group_{group_name}' - elif type(owner) is User: - user_name = owner.username - initial = f'user_{user_name}' - - return initial - def get_form(self): """ Customize form data for StockLocation editing. @@ -162,12 +144,12 @@ class StockLocationEdit(AjaxUpdateView): # Is ownership control enabled? stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') - owner_initial = self.get_owner_initial() + owner = location.owner if not stock_ownership_control: form.fields['owner'].widget = HiddenInput() else: - form.fields['owner'].initial = owner_initial + form.fields['owner'].initial = owner if location.parent: form.fields['owner'].initial = location.parent.owner if not self.request.user.is_superuser: @@ -180,21 +162,14 @@ class StockLocationEdit(AjaxUpdateView): - update all children's owners with location's owner """ - self.object = form.save(commit=False) - - # parent = form.cleaned_data.get('parent', None) + self.object = form.save() stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') if stock_ownership_control: - owner = form.cleaned_data.get('owner', None) - self.object.save(**{'owner': owner}) - if self.object.get_children(): for child in self.object.get_children(): child.owner = self.object.owner child.save() - else: - self.object.save() return self.object @@ -1386,10 +1361,18 @@ class StockItemEdit(AjaxUpdateView): 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) + location_owner = location.owner + # Check if location has owner + if location_owner: + # Check location owner type and filter + if type(location_owner.owner) is Group: + queryset = location_owner.get_users() + if self.request.user in queryset: + form.fields['owner'].initial = self.request.user + form.fields['owner'].queryset = queryset + elif type(location_owner.owner) is User: + form.fields['owner'].disabled = True + form.fields['owner'].initial = location_owner return form @@ -1492,13 +1475,12 @@ class StockLocationCreate(AjaxCreateView): self.object = form.save(commit=False) parent = form.cleaned_data.get('parent', None) - owner = form.cleaned_data.get('owner', None) if parent: + # Select parent's owner self.object.owner = parent.owner - self.object.save() - else: - self.object.save(**{'owner': owner}) + + self.object.save() return self.object @@ -1734,13 +1716,17 @@ class StockItemCreate(AjaxCreateView): 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) + location_owner = location.owner + if location_owner: + # Check location owner type and filter + if type(location_owner.owner) is Group: + queryset = location_owner.get_users() if self.request.user in queryset: form.fields['owner'].initial = self.request.user form.fields['owner'].queryset = queryset + elif type(location_owner.owner) is User: + form.fields['owner'].disabled = True + form.fields['owner'].initial = location_owner return form diff --git a/InvenTree/users/apps.py b/InvenTree/users/apps.py index 07e303c1be..4f7ed3930c 100644 --- a/InvenTree/users/apps.py +++ b/InvenTree/users/apps.py @@ -16,6 +16,11 @@ class UsersConfig(AppConfig): except (OperationalError, ProgrammingError): pass + try: + self.update_owners() + except (OperationalError, ProgrammingError): + pass + def assign_permissions(self): from django.contrib.auth.models import Group @@ -31,3 +36,9 @@ class UsersConfig(AppConfig): for group in Group.objects.all(): update_group_roles(group) + + def update_owners(self): + + from users.models import create_owners + + create_owners(full_update=True) diff --git a/InvenTree/users/migrations/0004_owner_model.py b/InvenTree/users/migrations/0004_owner_model.py new file mode 100644 index 0000000000..e019d21d13 --- /dev/null +++ b/InvenTree/users/migrations/0004_owner_model.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.7 on 2021-01-11 18:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('users', '0003_auto_20201005_2227'), + ] + + operations = [ + migrations.CreateModel( + name='Owner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('owner_id', models.PositiveIntegerField(blank=True, null=True)), + ('owner_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + ), + migrations.AddConstraint( + model_name='owner', + constraint=models.UniqueConstraint(fields=('owner_type', 'owner_id'), name='unique_owner'), + ), + ] diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index b54cddf7c4..f231c708ca 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- -from django.contrib.auth.models import Group, Permission +from django.contrib.auth.models import User, Group, Permission +from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType +from django.db.models import UniqueConstraint +from django.db.utils import IntegrityError from django.db import models from django.utils.translation import gettext_lazy as _ @@ -385,3 +388,79 @@ def check_user_role(user, role, permission): # No matching permissions found return False + + +class Owner(models.Model): + """ + An owner is either a group or user. + Owner can be associated to any InvenTree model (part, stock, etc.) + """ + + class Meta: + constraints = [ + UniqueConstraint(fields=['owner_type', 'owner_id'], + name='unique_owner') + ] + + owner_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) + owner_id = models.PositiveIntegerField(null=True, blank=True) + owner = GenericForeignKey('owner_type', 'owner_id') + + def __str__(self): + return f'{self.owner} ({self.owner_type.name})' + + def get_users(self): + + owner_users = None + + if type(self.owner) is Group: + users = User.objects.filter(groups__name=self.owner.name) + owner_users = Owner.objects.filter(owner_id__in=users, + owner_type=ContentType.objects.get_for_model(User).id) + + return owner_users + + +def create_owners(full_update=False, group=None, user=None): + """ Create all owners """ + + if full_update: + # Create group owners + for group in Group.objects.all(): + try: + Owner.objects.create(owner=group) + except IntegrityError: + pass + + # Create user owners + for user in User.objects.all(): + try: + Owner.objects.create(owner=user) + except IntegrityError: + pass + else: + if group: + try: + Owner.objects.create(owner=group) + except IntegrityError: + pass + + if user: + try: + Owner.objects.create(owner=user) + except IntegrityError: + pass + + +@receiver(post_save, sender=Group) +def create_new_owner_group(sender, instance, **kwargs): + """ Called *after* a Group object is saved. """ + + create_owners(group=instance) + + +@receiver(post_save, sender=User) +def create_new_owner_user(sender, instance, **kwargs): + """ Called *after* a User object is saved. """ + + create_owners(user=instance)