mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Migrate "Convert to Variant" form to the API (#3183)
* Adds a Part API filter to limit query to valid conversion options for the specified part * Refactor 'exclude_tree' filter to use django-filter framework * Refactor the 'ancestor' filter * Refactoring more API filtering fields: - variant_of - in_bom_for * Adds API endpoint / view / serializer for converting a StockItem to variant * stock item conversion now perfomed via the API * Bump API version * Add unit tests for new filtering option on the Part list API endpoint * Adds unit test for "convert" API endpoint functionality
This commit is contained in:
		@@ -810,6 +810,53 @@ class PartFilter(rest_filters.FilterSet):
 | 
			
		||||
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    convert_from = rest_filters.ModelChoiceFilter(label="Can convert from", queryset=Part.objects.all(), method='filter_convert_from')
 | 
			
		||||
 | 
			
		||||
    def filter_convert_from(self, queryset, name, part):
 | 
			
		||||
        """Limit the queryset to valid conversion options for the specified part"""
 | 
			
		||||
        conversion_options = part.get_conversion_options()
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.filter(pk__in=conversion_options)
 | 
			
		||||
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    exclude_tree = rest_filters.ModelChoiceFilter(label="Exclude Part tree", queryset=Part.objects.all(), method='filter_exclude_tree')
 | 
			
		||||
 | 
			
		||||
    def filter_exclude_tree(self, queryset, name, part):
 | 
			
		||||
        """Exclude all parts and variants 'down' from the specified part from the queryset"""
 | 
			
		||||
 | 
			
		||||
        children = part.get_descendants(include_self=True)
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.exclude(id__in=children)
 | 
			
		||||
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    ancestor = rest_filters.ModelChoiceFilter(label='Ancestor', queryset=Part.objects.all(), method='filter_ancestor')
 | 
			
		||||
 | 
			
		||||
    def filter_ancestor(self, queryset, name, part):
 | 
			
		||||
        """Limit queryset to descendants of the specified ancestor part"""
 | 
			
		||||
 | 
			
		||||
        descendants = part.get_descendants(include_self=False)
 | 
			
		||||
        queryset = queryset.filter(id__in=descendants)
 | 
			
		||||
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    variant_of = rest_filters.ModelChoiceFilter(label='Variant Of', queryset=Part.objects.all(), method='filter_variant_of')
 | 
			
		||||
 | 
			
		||||
    def filter_variant_of(self, queryset, name, part):
 | 
			
		||||
        """Limit queryset to direct children (variants) of the specified part"""
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.filter(id__in=part.get_children())
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    in_bom_for = rest_filters.ModelChoiceFilter(label='In BOM Of', queryset=Part.objects.all(), method='filter_in_bom')
 | 
			
		||||
 | 
			
		||||
    def filter_in_bom(self, queryset, name, part):
 | 
			
		||||
        """Limit queryset to parts in the BOM for the specified part"""
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.filter(id__in=part.get_parts_in_bom())
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    is_template = rest_filters.BooleanFilter()
 | 
			
		||||
 | 
			
		||||
    assembly = rest_filters.BooleanFilter()
 | 
			
		||||
@@ -1129,61 +1176,6 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
 | 
			
		||||
 | 
			
		||||
            queryset = queryset.exclude(pk__in=id_values)
 | 
			
		||||
 | 
			
		||||
        # 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)
 | 
			
		||||
 | 
			
		||||
        if ancestor is not None:
 | 
			
		||||
            # If an 'ancestor' part is provided, filter to match only children
 | 
			
		||||
            try:
 | 
			
		||||
                ancestor = Part.objects.get(pk=ancestor)
 | 
			
		||||
                descendants = ancestor.get_descendants(include_self=False)
 | 
			
		||||
                queryset = queryset.filter(pk__in=[d.pk for d in descendants])
 | 
			
		||||
            except (ValueError, Part.DoesNotExist):
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        # Filter by 'variant_of'
 | 
			
		||||
        # Note that this is subtly different from 'ancestor' filter (above)
 | 
			
		||||
        variant_of = params.get('variant_of', None)
 | 
			
		||||
 | 
			
		||||
        if variant_of is not None:
 | 
			
		||||
            try:
 | 
			
		||||
                template = Part.objects.get(pk=variant_of)
 | 
			
		||||
                variants = template.get_children()
 | 
			
		||||
                queryset = queryset.filter(pk__in=[v.pk for v in variants])
 | 
			
		||||
            except (ValueError, Part.DoesNotExist):
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        # Filter only parts which are in the "BOM" for a given part
 | 
			
		||||
        in_bom_for = params.get('in_bom_for', None)
 | 
			
		||||
 | 
			
		||||
        if in_bom_for is not None:
 | 
			
		||||
            try:
 | 
			
		||||
                in_bom_for = Part.objects.get(pk=in_bom_for)
 | 
			
		||||
 | 
			
		||||
                # Extract a list of parts within the BOM
 | 
			
		||||
                bom_parts = in_bom_for.get_parts_in_bom()
 | 
			
		||||
                print("bom_parts:", bom_parts)
 | 
			
		||||
                print([p.pk for p in bom_parts])
 | 
			
		||||
 | 
			
		||||
                queryset = queryset.filter(pk__in=[p.pk for p in bom_parts])
 | 
			
		||||
 | 
			
		||||
            except (ValueError, Part.DoesNotExist):
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        # Filter by whether the BOM has been validated (or not)
 | 
			
		||||
        bom_valid = params.get('bom_valid', None)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -391,6 +391,64 @@ class PartAPITest(InvenTreeAPITestCase):
 | 
			
		||||
        response = self.get(url, {'related': 1}, expected_code=200)
 | 
			
		||||
        self.assertEqual(len(response.data), 2)
 | 
			
		||||
 | 
			
		||||
    def test_filter_by_convert(self):
 | 
			
		||||
        """Test that we can correctly filter the Part list by conversion options"""
 | 
			
		||||
 | 
			
		||||
        category = PartCategory.objects.get(pk=3)
 | 
			
		||||
 | 
			
		||||
        # First, construct a set of template / variant parts
 | 
			
		||||
        master_part = Part.objects.create(
 | 
			
		||||
            name='Master', description='Master part',
 | 
			
		||||
            category=category,
 | 
			
		||||
            is_template=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Construct a set of variant parts
 | 
			
		||||
        variants = []
 | 
			
		||||
 | 
			
		||||
        for color in ['Red', 'Green', 'Blue', 'Yellow', 'Pink', 'Black']:
 | 
			
		||||
            variants.append(Part.objects.create(
 | 
			
		||||
                name=f"{color} Variant", description="Variant part with a specific color",
 | 
			
		||||
                variant_of=master_part,
 | 
			
		||||
                category=category,
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
        url = reverse('api-part-list')
 | 
			
		||||
 | 
			
		||||
        # An invalid part ID will return an error
 | 
			
		||||
        response = self.get(
 | 
			
		||||
            url,
 | 
			
		||||
            {
 | 
			
		||||
                'convert_from': 999999,
 | 
			
		||||
            },
 | 
			
		||||
            expected_code=400
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertIn('Select a valid choice', str(response.data['convert_from']))
 | 
			
		||||
 | 
			
		||||
        for variant in variants:
 | 
			
		||||
            response = self.get(
 | 
			
		||||
                url,
 | 
			
		||||
                {
 | 
			
		||||
                    'convert_from': variant.pk,
 | 
			
		||||
                },
 | 
			
		||||
                expected_code=200
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # There should be the same number of results for each request
 | 
			
		||||
            self.assertEqual(len(response.data), 6)
 | 
			
		||||
 | 
			
		||||
            id_values = [p['pk'] for p in response.data]
 | 
			
		||||
 | 
			
		||||
            self.assertIn(master_part.pk, id_values)
 | 
			
		||||
 | 
			
		||||
            for v in variants:
 | 
			
		||||
                # Check that all *other* variants are included also
 | 
			
		||||
                if v == variant:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                self.assertIn(v.pk, id_values)
 | 
			
		||||
 | 
			
		||||
    def test_include_children(self):
 | 
			
		||||
        """Test the special 'include_child_categories' flag.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user