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
This commit is contained in:
Oliver 2023-10-23 21:35:51 +11:00 committed by GitHub
parent 2dfe2d97bc
commit 39c499622d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 138 additions and 8 deletions

View File

@ -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

View File

@ -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,)

View File

@ -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<key>[-:\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/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),

View File

@ -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'),
),
]

View File

@ -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,
)
]

View File

@ -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',
),
]

View File

@ -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,

View File

@ -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)

View File

@ -384,11 +384,11 @@
<td>{% include 'clip_link.html' with link=part.link new_window=True %}</td>
</tr>
{% endif %}
{% if part.responsible %}
{% if part.responsible_owner %}
<tr>
<td><span class='fas fa-user'></span></td>
<td>{% trans "Responsible" %}</td>
<td> <span class='badge badge-right rounded-pill bg-dark'>{{ part.responsible }}</span></td>
<td> <span class='badge badge-right rounded-pill bg-dark'>{{ part.responsible_owner }}</span></td>
</tr>
{% endif %}
</table>