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/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/part/api.py b/InvenTree/part/api.py index 39801070c7..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 = [ @@ -644,6 +658,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) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 49be12e283..4cecd12f17 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 @@ -359,6 +360,17 @@ 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 @@ -414,7 +426,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/stock/api.py b/InvenTree/stock/api.py index 56df35b5c3..cf58c7d4d9 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=[subloc.pk for subloc in loc.get_descendants(include_self=True)] + ) + + except (ValueError, StockLocation.DoesNotExist): + pass + return queryset filter_backends = [ @@ -719,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, 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