From 39c499622dc031edec5340a43e5d49090dfad243 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 23 Oct 2023 21:35:51 +1100 Subject: [PATCH] Part responsible owner (#5774) * Add "responsible_owner" field to part model - Will replace "responsible" field * Data migration - Adds 'responsible_owner' value for parts which have 'responsible' set - Selects correct content type - Performs reverse migratoin * Update part serializer - Point to the new field - Rename to preserve compatibility - OPTIONS metadata will take care of the rest * Remove old 'responsible' field * Bump API version * Fix typo * Fix serializer field --- InvenTree/InvenTree/api_version.py | 5 +- InvenTree/InvenTree/social_auth_urls.py | 2 +- InvenTree/InvenTree/urls.py | 4 +- .../migrations/0115_part_responsible_owner.py | 20 +++++ .../migrations/0116_auto_20231023_0332.py | 76 +++++++++++++++++++ .../0117_remove_part_responsible.py | 17 +++++ InvenTree/part/models.py | 11 ++- InvenTree/part/serializers.py | 7 ++ InvenTree/part/templates/part/part_base.html | 4 +- 9 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 InvenTree/part/migrations/0115_part_responsible_owner.py create mode 100644 InvenTree/part/migrations/0116_auto_20231023_0332.py create mode 100644 InvenTree/part/migrations/0117_remove_part_responsible.py diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index d272ac54e6..c3dd8a1897 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,14 @@ # InvenTree API version -INVENTREE_API_VERSION = 140 +INVENTREE_API_VERSION = 141 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v141 -> 2023-10-23 : https://github.com/inventree/InvenTree/pull/5774 + - Changed 'part.responsible' from User to Owner + v140 -> 2023-10-20 : https://github.com/inventree/InvenTree/pull/5664 - Expand API token functionality - Multiple API tokens can be generated per user diff --git a/InvenTree/InvenTree/social_auth_urls.py b/InvenTree/InvenTree/social_auth_urls.py index 304232ea9a..fe6a129673 100644 --- a/InvenTree/InvenTree/social_auth_urls.py +++ b/InvenTree/InvenTree/social_auth_urls.py @@ -96,7 +96,7 @@ for provider in providers.registry.get_list(): social_auth_urlpatterns += provider_urlpatterns -class SocialProvierListView(ListAPIView): +class SocialProviderListView(ListAPIView): """List of available social providers.""" permission_classes = (AllowAny,) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index a331b1ecc9..82a21fd331 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -38,7 +38,7 @@ from web.urls import urlpatterns as platform_urls from .api import APISearchView, InfoView, NotFoundView from .magic_login import GetSimpleLoginView -from .social_auth_urls import SocialProvierListView, social_auth_urlpatterns +from .social_auth_urls import SocialProviderListView, social_auth_urlpatterns from .views import (AboutView, AppearanceSelectView, CustomConnectionsView, CustomEmailView, CustomLoginView, CustomPasswordResetFromKeyView, @@ -83,7 +83,7 @@ apipatterns = [ path('auth/', include([ re_path(r'^registration/account-confirm-email/(?P[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'), path('registration/', include('dj_rest_auth.registration.urls')), - path('providers/', SocialProvierListView.as_view(), name='social_providers'), + path('providers/', SocialProviderListView.as_view(), name='social_providers'), path('social/', include(social_auth_urlpatterns)), path('social/', SocialAccountListView.as_view(), name='social_account_list'), path('social//disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'), diff --git a/InvenTree/part/migrations/0115_part_responsible_owner.py b/InvenTree/part/migrations/0115_part_responsible_owner.py new file mode 100644 index 0000000000..2605ac51f2 --- /dev/null +++ b/InvenTree/part/migrations/0115_part_responsible_owner.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.22 on 2023-10-23 01:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0009_auto_20231020_2356'), + ('part', '0114_alter_part_minimum_stock'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='responsible_owner', + field=models.ForeignKey(blank=True, help_text='Owner responsible for this part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parts_responsible', to='users.owner', verbose_name='Responsible'), + ), + ] diff --git a/InvenTree/part/migrations/0116_auto_20231023_0332.py b/InvenTree/part/migrations/0116_auto_20231023_0332.py new file mode 100644 index 0000000000..49f3f675a9 --- /dev/null +++ b/InvenTree/part/migrations/0116_auto_20231023_0332.py @@ -0,0 +1,76 @@ +# Generated by Django 3.2.22 on 2023-10-23 03:32 + +from django.db import migrations + + +def migrate_part_responsible_owner(apps, schema_editor): + """Copy existing part.responsible field to part.responsible_owner""" + + Owner = apps.get_model('users', 'Owner') + Part = apps.get_model('part', 'Part') + User = apps.get_model('auth', 'user') + ContentType = apps.get_model('contenttypes', 'contenttype') + + user_type = ContentType.objects.get_for_model(User) + + parts = Part.objects.exclude(responsible=None) + + for part in parts: + + # Find a corresponding Owner object, or create one if it does not exist + owner, _created = Owner.objects.get_or_create( + owner_type=user_type, + owner_id=part.responsible.id, + ) + + part.responsible_owner = owner + part.save() + + if parts.count() > 0: + print(f"Added 'responsible_owner' for {parts.count()} parts") + + +def reverse_owner_migration(apps, schema_editor): + """Reverse the owner migration: + + - Set the 'responsible' field to a selected user + - Only where 'responsible_owner' is set + - Only where 'responsible_owner' is a User object + """ + + Part = apps.get_model('part', 'Part') + User = apps.get_model('auth', 'user') + ContentType = apps.get_model('contenttypes', 'contenttype') + + user_type = ContentType.objects.get_for_model(User) + + parts = Part.objects.exclude(responsible_owner=None) + + for part in parts: + + if part.responsible_owner.owner_type == user_type: + + # Attempt to find matching user + try: + user = User.objects.get(pk=part.responsible_owner.owner_id) + part.responsible = user + part.save() + except User.DoesNotExist: + print("User does not exist:", part.responsible_owner.owner_id) + + if parts.count() > 0: + print(f"Added 'responsible' for {parts.count()} parts") + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0115_part_responsible_owner'), + ('users', '0005_owner_model'), + ] + + operations = [ + migrations.RunPython( + migrate_part_responsible_owner, + reverse_code=reverse_owner_migration, + ) + ] diff --git a/InvenTree/part/migrations/0117_remove_part_responsible.py b/InvenTree/part/migrations/0117_remove_part_responsible.py new file mode 100644 index 0000000000..add8405179 --- /dev/null +++ b/InvenTree/part/migrations/0117_remove_part_responsible.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.22 on 2023-10-23 05:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0116_auto_20231023_0332'), + ] + + operations = [ + migrations.RemoveField( + model_name='part', + name='responsible', + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 854deba245..2960d8335b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -41,6 +41,7 @@ import InvenTree.fields import InvenTree.ready import InvenTree.tasks import part.settings as part_settings +import users.models from build import models as BuildModels from common.models import InvenTreeSetting from common.settings import currency_code_default @@ -379,7 +380,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel) notes: Additional notes field for this part creation_date: Date that this part was added to the database creation_user: User who added this part to the database - responsible: User who is responsible for this part (optional) + responsible_owner: Owner (either user or group) which is responsible for this part (optional) last_stocktake: Date at which last stocktake was performed for this Part """ @@ -1036,7 +1037,13 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel) creation_user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Creation User'), related_name='parts_created') - responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Responsible'), help_text=_('User responsible for this part'), related_name='parts_responible') + responsible_owner = models.ForeignKey( + users.models.Owner, on_delete=models.SET_NULL, + blank=True, null=True, + verbose_name=_('Responsible'), + help_text=_('Owner responsible for this part'), + related_name='parts_responsible' + ) last_stocktake = models.DateField( blank=True, null=True, diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index ab6881c1b0..f30ba199fd 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -26,6 +26,7 @@ import part.filters import part.stocktake import part.tasks import stock.models +import users.models from InvenTree.status_codes import BuildStatusGroups from InvenTree.tasks import offload_task @@ -695,6 +696,12 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize read_only=True, ) + responsible = serializers.PrimaryKeyRelatedField( + queryset=users.models.Owner.objects.all(), + required=False, allow_null=True, + source='responsible_owner', + ) + # Annotated fields allocated_to_build_orders = serializers.FloatField(read_only=True) allocated_to_sales_orders = serializers.FloatField(read_only=True) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index eaec63ce97..3dcfeebb78 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -384,11 +384,11 @@ {% include 'clip_link.html' with link=part.link new_window=True %} {% endif %} - {% if part.responsible %} + {% if part.responsible_owner %} {% trans "Responsible" %} - {{ part.responsible }} + {{ part.responsible_owner }} {% endif %}