From dad9239a1c37d43c493ea3c7ee3f4157cb21ce18 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 20:59:55 +1000 Subject: [PATCH 1/6] Add instance-specific filters to API OPTIONS data --- InvenTree/InvenTree/metadata.py | 39 +++++++++++++++++++++++++++++++++ InvenTree/InvenTree/models.py | 11 ++++++++++ InvenTree/part/models.py | 12 ++++++++++ 3 files changed, 62 insertions(+) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index c22b39dc43..6cf29ab945 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -32,6 +32,9 @@ class InvenTreeMetadata(SimpleMetadata): def determine_metadata(self, request, view): + self.request = request + self.view = view + metadata = super().determine_metadata(request, view) user = request.user @@ -136,6 +139,42 @@ class InvenTreeMetadata(SimpleMetadata): except AttributeError: pass + # Try to extract 'instance' information + instance = None + + # Extract extra information if an instance is available + if hasattr(serializer, 'instance'): + instance = serializer.instance + + if instance is None: + try: + instance = self.view.get_object() + except: + pass + + if instance is not None: + """ + If there is an instance associated with this API View, + introspect that instance to find any specific API info. + """ + + if hasattr(instance, 'api_instance_filters'): + + instance_filters = instance.api_instance_filters() + + for field_name, field_filters in instance_filters.items(): + + if field_name not in serializer_info.keys(): + # The field might be missing, but is added later on + # This function seems to get called multiple times? + continue + + if 'instance_filters' not in serializer_info[field_name].keys(): + serializer_info[field_name]['instance_filters'] = {} + + for key, value in field_filters.items(): + serializer_info[field_name]['instance_filters'][key] = value + return serializer_info def get_field_info(self, field): diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 2831a23151..3213838e78 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -93,6 +93,17 @@ class InvenTreeTree(MPTTModel): parent: The item immediately above this one. An item with a null parent is a top-level item """ + def api_instance_filters(self): + """ + Instance filters for InvenTreeTree models + """ + + return { + 'parent': { + 'exclude_tree': self.pk, + } + } + def save(self, *args, **kwargs): try: diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 49be12e283..23436a7d6a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -359,6 +359,18 @@ class Part(MPTTModel): return reverse('api-part-list') + def api_instance_filters(self): + """ + Return API query filters for limiting field results against this instance + """ + + return { + 'variant_of': { + 'exclude_tree': self.pk, + } + } + + def get_context_data(self, request, **kwargs): """ Return some useful context data about this part for template rendering From df48df8119f60f7b4d52e9d2068bab7b4e3e8b2e Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:10:31 +1000 Subject: [PATCH 2/6] Catch recursive tree error for part / variant relationship --- InvenTree/part/models.py | 8 +++++++- InvenTree/templates/js/forms.js | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 23436a7d6a..d5e039ca77 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -27,6 +27,7 @@ from markdownx.models import MarkdownxField from django_cleanup import cleanup from mptt.models import TreeForeignKey, MPTTModel +from mptt.exceptions import InvalidMove from mptt.managers import TreeManager from stdimage.models import StdImageField @@ -426,7 +427,12 @@ class Part(MPTTModel): self.full_clean() - super().save(*args, **kwargs) + try: + super().save(*args, **kwargs) + except InvalidMove: + raise ValidationError({ + 'variant_of': _('Invalid choice for parent part'), + }) if add_category_templates: # Get part category diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 587ea07a16..4801ec77eb 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -350,6 +350,12 @@ function constructFormBody(fields, options) { for(field in fields) { fields[field].name = field; + + // If any "instance_filters" are defined for the endpoint, copy them across (overwrite) + if (fields[field].instance_filters) { + fields[field].filters = Object.assign(fields[field].filters || {}, fields[field].instance_filters); + } + var field_options = displayed_fields[field]; // Copy custom options across to the fields object From bee0a519ef71b6e778ad56ae5b0e971ceac46104 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:18:01 +1000 Subject: [PATCH 3/6] Allow filtering of PartList by exclude_tree --- InvenTree/part/api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 39801070c7..527a9395ee 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -644,6 +644,20 @@ class PartList(generics.ListCreateAPIView): except (ValueError, Part.DoesNotExist): pass + # Exclude part variant tree? + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + try: + top_level_part = Part.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[prt.pk for prt in top_level_part.get_descendants(include_self=True)] + ) + + except (ValueError, Part.DoesNotExist): + pass + # Filter by 'ancestor'? ancestor = params.get('ancestor', None) From 85a40ec41877d5f10555cf568395279a4b04c6a8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:23:30 +1000 Subject: [PATCH 4/6] Tree exclusion for PartCategory and StockLocation --- InvenTree/part/api.py | 14 ++++++++++++++ InvenTree/stock/api.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 527a9395ee..28e5d4300a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -105,6 +105,20 @@ class CategoryList(generics.ListCreateAPIView): except (ValueError, PartCategory.DoesNotExist): pass + # Exclude PartCategory tree + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + try: + cat = PartCategory.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[c.pk for c in cat.get_descendants(include_self=True)] + ) + + except (ValueError, PartCategory.DoesNotExist): + pass + return queryset filter_backends = [ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 56df35b5c3..ce5e902cff 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -343,6 +343,20 @@ class StockLocationList(generics.ListCreateAPIView): except (ValueError, StockLocation.DoesNotExist): pass + # Exclude StockLocation tree + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + try: + loc = StockLocation.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[l.pk for l in loc.get_descendants(include_self=True)] + ) + + except (ValueError, StockLocation.DoesNotExist): + pass + return queryset filter_backends = [ From 9cf372f633d7be143e4a70da516d4018101599ff Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:24:18 +1000 Subject: [PATCH 5/6] PEP fixes --- InvenTree/part/models.py | 1 - InvenTree/stock/api.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d5e039ca77..4cecd12f17 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -371,7 +371,6 @@ class Part(MPTTModel): } } - def get_context_data(self, request, **kwargs): """ Return some useful context data about this part for template rendering diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index ce5e902cff..b11f556e34 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -351,7 +351,7 @@ class StockLocationList(generics.ListCreateAPIView): loc = StockLocation.objects.get(pk=exclude_tree) queryset = queryset.exclude( - pk__in=[l.pk for l in loc.get_descendants(include_self=True)] + pk__in=[subloc.pk for subloc in loc.get_descendants(include_self=True)] ) except (ValueError, StockLocation.DoesNotExist): From 4ee0004c97e8fdeb05d19ca891376fb5b9fae216 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:34:16 +1000 Subject: [PATCH 6/6] Filtering for Build and StockItem --- InvenTree/build/api.py | 15 +++++++++++++++ InvenTree/build/models.py | 8 ++++++++ InvenTree/stock/api.py | 14 ++++++++++++++ InvenTree/stock/models.py | 11 +++++++++++ 4 files changed, 48 insertions(+) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 47edb55545..eb6d42cc6d 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -104,6 +104,21 @@ class BuildList(generics.ListCreateAPIView): params = self.request.query_params + # exclude parent tree + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + + try: + build = Build.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[bld.pk for bld in build.get_descendants(include_self=True)] + ) + + except (ValueError, Build.DoesNotExist): + pass + # Filter by "parent" parent = params.get('parent', None) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 5f8af9096b..084f9ab2db 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -96,6 +96,14 @@ class Build(MPTTModel): def get_api_url(): return reverse('api-build-list') + def api_instance_filters(self): + + return { + 'parent': { + 'exclude_tree': self.pk, + } + } + def save(self, *args, **kwargs): try: diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index b11f556e34..cf58c7d4d9 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -733,6 +733,20 @@ class StockList(generics.ListCreateAPIView): if customer: queryset = queryset.filter(customer=customer) + # Exclude stock item tree + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + try: + item = StockItem.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[it.pk for it in item.get_descendants(include_self=True)] + ) + + except (ValueError, StockItem.DoesNotExist): + pass + # Filter by 'allocated' parts? allocated = params.get('allocated', None) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index c2122f40ac..11d5de9bf1 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -191,6 +191,17 @@ class StockItem(MPTTModel): def get_api_url(): return reverse('api-stock-list') + def api_instance_filters(self): + """ + Custom API instance filters + """ + + return { + 'parent': { + 'exclude_tree': self.pk, + } + } + # A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock" IN_STOCK_FILTER = Q( quantity__gt=0,