Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Approval #5559

Closed
wants to merge 76 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
41deab3
Approvals upstreaming (#2)
matmair Jun 23, 2023
f054d45
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Jun 23, 2023
d5c659e
cleanup
matmair Jun 23, 2023
3a58900
another cleanup
matmair Jun 23, 2023
4b5f859
check already in init
matmair Jun 24, 2023
b6449a7
fix typo
matmair Jun 24, 2023
8138a67
extend admin
matmair Jun 24, 2023
d032e71
add test for ApprovalRule
matmair Jun 24, 2023
ca85c9c
add dedicated approval view
matmair Jun 25, 2023
767386e
fix test/check
matmair Jun 25, 2023
03b16b9
Add PO mechanisms
matmair Jun 26, 2023
27e03e7
Add migration
matmair Jun 30, 2023
7cdb5e2
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Aug 5, 2023
bd49a19
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Aug 6, 2023
4275fed
merge fix
matmair Aug 6, 2023
27661e1
added po overview page for P-UI
matmair Aug 7, 2023
70383a5
add more renderers
matmair Aug 8, 2023
f7aa087
simplify render statements
matmair Aug 8, 2023
b1d52fd
add types
matmair Aug 8, 2023
658d9be
use smaller size for status
matmair Aug 9, 2023
9cfb9b0
Added owner renderer
matmair Aug 10, 2023
e5a13aa
refactored OwnerRenderer
matmair Aug 10, 2023
4bec39c
fixed names and urls to purchase order
matmair Aug 10, 2023
3528f47
added po detail page
matmair Aug 10, 2023
6796996
switched from dict to component based routes
matmair Aug 10, 2023
a40b3b2
renamed file
matmair Aug 10, 2023
d8afff8
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Aug 10, 2023
0a346d1
fix merge
matmair Aug 10, 2023
9c4427f
added some placeholders for po detail page
matmair Aug 10, 2023
a31da79
Added approval UI
matmair Aug 13, 2023
918d690
add user_id to api
matmair Aug 13, 2023
e2de97c
changed: Only allow one approval per PO
matmair Aug 13, 2023
e394871
added apis to look up approvals
matmair Aug 13, 2023
9df6536
made small UI change
matmair Aug 13, 2023
0eaf2e1
moved info text down
matmair Aug 13, 2023
ab2857e
fixed type
matmair Aug 14, 2023
cee8370
added user/owner details to API
matmair Aug 14, 2023
4dc8389
simplified user_detail
matmair Aug 14, 2023
de9d5f1
split up test setup methods
matmair Aug 14, 2023
003ed8d
fixed serializer inheritance
matmair Aug 14, 2023
a074e8e
Added user render to approvals
matmair Aug 14, 2023
561da8f
changed decision serializer to always add user details
matmair Aug 14, 2023
f74189a
added small fixes
matmair Aug 15, 2023
d13dd73
added error handeling
matmair Aug 15, 2023
5a0c95d
fixed wrong type reference
matmair Aug 15, 2023
aa7aaf2
added generic approval lookup
matmair Aug 15, 2023
1c1cb9c
switched to use data directly
matmair Aug 15, 2023
764d9fe
added missing settings mapping
matmair Aug 15, 2023
280a945
Added creation notification
matmair Aug 16, 2023
5b9e19a
changed default name for PO approval
matmair Aug 16, 2023
cb91895
added checks to ensure there are responsible users for PO and approval
matmair Aug 16, 2023
80e6a01
added signals
matmair Aug 16, 2023
3917d6f
added signal handler for PO approval
matmair Aug 16, 2023
cdaa9ce
added approval lookup and check
matmair Aug 16, 2023
46f7725
extended state checks
matmair Aug 16, 2023
f8518f4
added canceled notification
matmair Aug 16, 2023
9f209e7
added ApprovalDecision notification
matmair Aug 16, 2023
a4bf109
removed build order cancel notification for now
matmair Aug 17, 2023
32bddfd
added frontend urls to API
matmair Aug 24, 2023
b0dfa50
made P UI base url changeable
matmair Aug 24, 2023
10b8af2
added layout component
matmair Aug 25, 2023
3c1b76a
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Sep 17, 2023
0227a99
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Sep 19, 2023
98130ed
updated table defintion
matmair Sep 19, 2023
90a2bd7
added POs to navigation
matmair Sep 19, 2023
5bc1425
fixed base P UI url
matmair Sep 20, 2023
9ed302a
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Sep 20, 2023
85a8e15
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Sep 21, 2023
241e39d
moved search query to strongly typed model reference
matmair Sep 22, 2023
e6bf470
move title and link to reusable typed section
matmair Sep 22, 2023
97997a5
typed ApiFormFieldType.model too
matmair Sep 22, 2023
c24c8c5
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Sep 22, 2023
4403293
Merge branch 'typed-drawer' of https://github.com/matmair/InvenTree i…
matmair Sep 22, 2023
d1df095
added modeltype to ApprovalBox
matmair Sep 22, 2023
be0285a
add approval frontend interfaces
matmair Sep 23, 2023
58da88c
added way to modify primary key in table
matmair Sep 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
/InvenTree/plugin/ @SchrodingersGat @matmair
/InvenTree/plugins/ @SchrodingersGat @matmair

# specialised modules
/InvenTree/approvals @matmair

# Installer functions
.pkgr.yml @matmair
Procfile @matmair
Expand Down
1 change: 1 addition & 0 deletions InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
'users.apps.UsersConfig',
'web',
'generic',
'approval.apps.ApprovalConfig',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last

# Core django modules
Expand Down
3 changes: 3 additions & 0 deletions InvenTree/InvenTree/status_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class PurchaseOrderStatus(StatusCode):

# Order status codes
PENDING = 10, _("Pending"), 'secondary' # Order is pending (not yet placed)
PENDING_PLACING = 15, _("Pending placing"), 'secondary' # Order is pending an action for being placed
PENDING_APPROVAL = 16, _("Pending approval"), 'secondary' # Order is pending approval
PLACED = 20, _("Placed"), 'primary' # Order has been placed with supplier
COMPLETE = 30, _("Complete"), 'success' # Order has been completed
CANCELLED = 40, _("Cancelled"), 'danger' # Order was cancelled
Expand All @@ -23,6 +25,7 @@ class PurchaseOrderStatusGroups:
# Open orders
OPEN = [
PurchaseOrderStatus.PENDING.value,
PurchaseOrderStatus.PENDING_PLACING.value,
PurchaseOrderStatus.PLACED.value,
]

Expand Down
2 changes: 2 additions & 0 deletions InvenTree/InvenTree/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
from sesame.views import LoginView

from approval.api import approval_api_urls
from build.api import build_api_urls
from build.urls import build_urls
from common.api import admin_api_urls, common_api_urls, settings_api_urls
Expand Down Expand Up @@ -66,6 +67,7 @@
re_path(r'^report/', include(report_api_urls)),
re_path(r'^user/', include(user_urls)),
re_path(r'^admin/', include(admin_api_urls)),
re_path(r'^approval/', include(approval_api_urls)),

# Plugin endpoints
path('', include(plugin_api_urls)),
Expand Down
1 change: 1 addition & 0 deletions InvenTree/approval/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Approvals can be used to implement apprival or voting processes based on sets of rules."""
45 changes: 45 additions & 0 deletions InvenTree/approval/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Admin class definitions for approval app"""

from django.contrib import admin

from .models import Approval, ApprovalDecision


class ApprovalDecisionInline(admin.TabularInline):
"""Inline for ApprovalDecision."""

model = ApprovalDecision


@admin.register(Approval)
class ApprovalAdmin(admin.ModelAdmin):
"""Admin class for Approval model."""

resource_class = ApprovalDecisionInline

list_display = ('name', 'description', 'reference', 'finalised', 'status')

list_filter = [
'status',
'finalised',
'created_by',
'creation_date',
'modified_by',
'modified_date',
'finalised_by',
'finalised_date',
]

search_fields = [
'name',
'description',
'reference',
'created_by',
'creation_date',
'modified_by',
'modified_date',
'finalised_by',
'finalised_date',
]

inlines = [ApprovalDecisionInline,]
251 changes: 251 additions & 0 deletions InvenTree/approval/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
"""API views for the Approval app."""
from django.contrib.contenttypes.models import ContentType
from django.urls import include, path, re_path

from rest_framework import serializers
from rest_framework.generics import get_object_or_404

from InvenTree.api import MetadataView
from InvenTree.helpers import str2bool
from InvenTree.mixins import CreateAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
from InvenTree.serializers import InvenTreeModelSerializer, UserSerializer
from users.serializers import OwnerSerializer

from .models import Approval, ApprovalDecision


class UserDetailApiMixin:
"""Mixin to add extra context information regarding user/owner details to endpoint serializers."""

def get_serializer(self, *args, **kwargs):
"""Add extra context information to the endpoint serializer."""
try:
user_detail = str2bool(self.request.GET.get('user_detail', None))
except AttributeError:
user_detail = None

kwargs['user_detail'] = user_detail
return super().get_serializer(*args, **kwargs)

def get_object(self):
"""Get the object to be viewed."""
# Use default behaviour if no type is specified
if self.kwargs.get('type', None) is None:
return super().get_object()

# Alternatively, use the generic relationship to get the object
queryset = self.filter_queryset(self.get_queryset())
obj = get_object_or_404(queryset, **{'content_type__model': self.kwargs['type'], 'object_id': self.kwargs['pk']})
self.check_object_permissions(self.request, obj)
return obj


class UserDetailSerializerMixin:
"""Mixin to determine if extra serializer fields are required."""

def __init__(self, *args, **kwargs):
"""Determine if extra serializer fields are required"""
user_detail = kwargs.pop('user_detail', False)

super().__init__(*args, **kwargs)

if user_detail is not True:
for field in self.Meta.user_detail_fields:
self.fields.pop(field)


class TaggedObjectRelatedField(serializers.RelatedField):
"""A custom field to use for the `tagged_object` generic relationship."""

def to_representation(self, value):
"""Serialize tagged objects to a simple textual representation."""
if hasattr(value, 'get_api_url'):
return f'{value.get_api_url()}{value.id}/'
if hasattr(value, 'get_absolute_url'):
return value.get_absolute_url()
else:
return value.id


class ApprovalDecisionSerializer(UserDetailSerializerMixin, InvenTreeModelSerializer):
"""Serializes an ApprovalDecision object"""

user_detail = UserSerializer(source='user', read_only=True, many=False)

class Meta:
"""Meta data for ApprovalDecisionSerializer"""
model = ApprovalDecision
exclude = [
'metadata',
]
user_detail_fields = ['user_detail',]

def is_valid(self, *, raise_exception=False):
"""Insert user to save request."""
request = self.context['request']
self.initial_data['user'] = request.user.pk
return super().is_valid(raise_exception=raise_exception)


class ApprovalSerializer(UserDetailSerializerMixin, InvenTreeModelSerializer):
"""Serializes an Approval object"""

status_text = serializers.CharField(source='get_status_display', read_only=True)
content_object = TaggedObjectRelatedField(read_only=True)
model = serializers.CharField(required=False, write_only=True)
decisions = ApprovalDecisionSerializer(many=True, read_only=True, user_detail=True)
creation_date = serializers.DateTimeField(format='iso-8601', required=False)
created_by_detail = UserSerializer(source='created_by', read_only=True, many=False)
modified_date = serializers.DateTimeField(format='iso-8601', required=False)
modified_by_detail = UserSerializer(source='modified_by', read_only=True, many=False)
finalised_date = serializers.DateTimeField(format='iso-8601', required=False)
finalised_by_detail = UserSerializer(source='finalised_by', read_only=True, many=False)
responsible_detail = OwnerSerializer(source='responsible', read_only=True)
owner_detail = OwnerSerializer(source='owner', read_only=True)

class Meta:
"""Meta data for ApprovalSerializer"""
model = Approval
exclude = [
'reference_int',
'metadata',
'data',
# 'object_id',
# 'content_type',
]

read_only_fields = [
'creation_date',
'finalised_date',
'modified_date',
]

user_detail_fields = [
'created_by_detail',
'modified_by_detail',
'finalised_by_detail',
'responsible_detail',
'owner_detail',
]

def is_valid(self, *, raise_exception=False):
"""User optional field 'model' to determine content_type."""
if 'model' in self.initial_data:
model = self.initial_data.pop('model')
mdl_splt = model.split('.')
content_type = ContentType.objects.get(app_label=mdl_splt[0], model=mdl_splt[1])
self.initial_data['content_type'] = content_type.pk
return super().is_valid(raise_exception=raise_exception)


class ApprovalList(UserDetailApiMixin, ListCreateAPI):
"""API endpoint for listing all Approval objects"""

queryset = Approval.objects.all()
serializer_class = ApprovalSerializer
filterset_fields = [
"owner",
"status",
"content_type",
"object_id",
]
search_fields = [
"name",
"description",
"reference",
]
ordering_field_aliases = {
'reference': ['reference_int', 'reference'],
}
ordering_fields = [
"name",
"status",
"finalised_date",
"modified_date",
"created_date",
'reference',
]
ordering = '-reference'

def clean_data(self, data: dict) -> dict:
"""Custom clean_data method to add current user."""
data = super().clean_data(data)
data['created_by'] = self.request.user.pk
return data


class ApprovalDetail(UserDetailApiMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of an Approval object"""

queryset = Approval.objects.all()
serializer_class = ApprovalSerializer


class ApprovalDecisionList(UserDetailApiMixin, ListCreateAPI):
"""API endpoint for listing all ApprovalDecision objects"""

queryset = ApprovalDecision.objects.all()
serializer_class = ApprovalDecisionSerializer
filterset_fields = [
"approval",
"user",
"status",
]
search_fields = [
"comment",
]
ordering_fields = [
"approval",
"user",
"status",
"date",
]


class ApprovalDecisionDetail(UserDetailApiMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of an ApprovalDecision object"""

queryset = ApprovalDecision.objects.all()
serializer_class = ApprovalDecisionSerializer


class ApprovalApproveSerializer(InvenTreeModelSerializer):
"""Serializes an ApprovalDecision object"""

class Meta:
"""Meta data for ApprovalDecisionSerializer"""
model = ApprovalDecision
exclude = ['metadata',]

def is_valid(self, *, raise_exception=False):
"""Insert data to save request."""
request = self.context['request']
self.initial_data['user'] = request.user.pk
self.initial_data['approval'] = request.parser_context['kwargs'].get('pk', None)
self.initial_data['decision'] = True
return super().is_valid(raise_exception=raise_exception)


class ApproveView(CreateAPI):
"""API endpoint to approve approval."""

queryset = ApprovalDecision.objects.all()
serializer_class = ApprovalApproveSerializer


detail_api = [
re_path(r"^decision/", include([
re_path(r"^$", ApprovalDecisionList.as_view(), name="api-approval-decision-list",),
re_path(r"^(?P<pk>\d+)/", ApprovalDecisionDetail.as_view(), name="api-approval-decision-detail",),
])),
re_path('approve/', ApproveView.as_view(), name='api-approval-approve'),
re_path(r'^metadata/', MetadataView.as_view(), {'model': Approval}, name='api-approval-metadata'),
re_path(r'^.*$', ApprovalDetail.as_view(), name='api-approval-detail'),
]


approval_api_urls = [
path(r'<str:type>:<int:pk>/', include(detail_api)),
path(r'<int:pk>/', include(detail_api)),
re_path(r'^.*$', ApprovalList.as_view(), name='api-approval-list'),
]
18 changes: 18 additions & 0 deletions InvenTree/approval/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""AppConfig for approval app."""

from django.apps import AppConfig


class ApprovalConfig(AppConfig):
"""AppConfig for approval app."""
name = 'approval'

def ready(self):
"""Run setup step when app is ready."""
self.collect_notification_methods()

def collect_notification_methods(self):
"""Collect all rule definitions."""
from approval.rules import registry

registry.collect()
Loading
Loading