mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 21:25:42 +00:00 
			
		
		
		
	Cherry pick changes from fe0d9c1923
				
					
				
			This commit is contained in:
		| @@ -1006,7 +1006,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models. | ||||
|             ) | ||||
|  | ||||
|             # Filter out "serialized" stock items, these cannot be auto-allocated | ||||
|             available_stock = available_stock.filter(Q(serial=None) | Q(serial='')) | ||||
|             available_stock = available_stock.filter(Q(serial=None) | Q(serial='')).distinct() | ||||
|  | ||||
|             if location: | ||||
|                 # Filter only stock items located "below" the specified location | ||||
|   | ||||
| @@ -385,7 +385,7 @@ class SupplierPartList(ListCreateDestroyAPIView): | ||||
|         company = params.get('company', None) | ||||
|  | ||||
|         if company is not None: | ||||
|             queryset = queryset.filter(Q(manufacturer_part__manufacturer=company) | Q(supplier=company)) | ||||
|             queryset = queryset.filter(Q(manufacturer_part__manufacturer=company) | Q(supplier=company)).distinct() | ||||
|  | ||||
|         return queryset | ||||
|  | ||||
|   | ||||
| @@ -209,13 +209,13 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model): | ||||
|     @property | ||||
|     def parts(self): | ||||
|         """Return SupplierPart objects which are supplied or manufactured by this company.""" | ||||
|         return SupplierPart.objects.filter(Q(supplier=self.id) | Q(manufacturer_part__manufacturer=self.id)) | ||||
|         return SupplierPart.objects.filter(Q(supplier=self.id) | Q(manufacturer_part__manufacturer=self.id)).distinct() | ||||
|  | ||||
|     @property | ||||
|     def stock_items(self): | ||||
|         """Return a list of all stock items supplied or manufactured by this company.""" | ||||
|         stock = apps.get_model('stock', 'StockItem') | ||||
|         return stock.objects.filter(Q(supplier_part__supplier=self.id) | Q(supplier_part__manufacturer_part__manufacturer=self.id)).all() | ||||
|         return stock.objects.filter(Q(supplier_part__supplier=self.id) | Q(supplier_part__manufacturer_part__manufacturer=self.id)).distinct() | ||||
|  | ||||
|  | ||||
| class CompanyAttachment(InvenTreeAttachment): | ||||
|   | ||||
| @@ -935,8 +935,8 @@ class PartFilter(rest_filters.FilterSet): | ||||
|  | ||||
|         if str2bool(value): | ||||
|             return queryset.exclude(q_a | q_b) | ||||
|         else: | ||||
|             return queryset.filter(q_a | q_b) | ||||
|  | ||||
|         return queryset.filter(q_a | q_b).distinct() | ||||
|  | ||||
|     stocktake = rest_filters.BooleanFilter(label="Has stocktake", method='filter_has_stocktake') | ||||
|  | ||||
| @@ -1153,7 +1153,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI): | ||||
|                 # Return any relationship which points to the part in question | ||||
|                 relation_filter = Q(part_1=related_part) | Q(part_2=related_part) | ||||
|  | ||||
|                 for relation in PartRelated.objects.filter(relation_filter): | ||||
|                 for relation in PartRelated.objects.filter(relation_filter).distinct(): | ||||
|  | ||||
|                     if relation.part_1.pk != pk: | ||||
|                         part_ids.add(relation.part_1.pk) | ||||
| @@ -1333,8 +1333,7 @@ class PartRelatedList(ListCreateAPI): | ||||
|         if part is not None: | ||||
|             try: | ||||
|                 part = Part.objects.get(pk=part) | ||||
|  | ||||
|                 queryset = queryset.filter(Q(part_1=part) | Q(part_2=part)) | ||||
|                 queryset = queryset.filter(Q(part_1=part) | Q(part_2=part)).distinct() | ||||
|  | ||||
|             except (ValueError, Part.DoesNotExist): | ||||
|                 pass | ||||
| @@ -1373,8 +1372,8 @@ class PartParameterTemplateFilter(rest_filters.FilterSet): | ||||
|  | ||||
|         if str2bool(value): | ||||
|             return queryset.exclude(Q(choices=None) | Q(choices='')) | ||||
|         else: | ||||
|             return queryset.filter(Q(choices=None) | Q(choices='')) | ||||
|  | ||||
|         return queryset.filter(Q(choices=None) | Q(choices='')).distinct() | ||||
|  | ||||
|     has_units = rest_filters.BooleanFilter( | ||||
|         method='filter_has_units', | ||||
| @@ -1386,8 +1385,8 @@ class PartParameterTemplateFilter(rest_filters.FilterSet): | ||||
|  | ||||
|         if str2bool(value): | ||||
|             return queryset.exclude(Q(units=None) | Q(units='')) | ||||
|         else: | ||||
|             return queryset.filter(Q(units=None) | Q(units='')) | ||||
|  | ||||
|         return queryset.filter(Q(units=None) | Q(units='')).distinct() | ||||
|  | ||||
|  | ||||
| class PartParameterTemplateList(ListCreateAPI): | ||||
| @@ -1653,8 +1652,8 @@ class BomFilter(rest_filters.FilterSet): | ||||
|  | ||||
|         if str2bool(value): | ||||
|             return queryset.exclude(q_a | q_b) | ||||
|         else: | ||||
|             return queryset.filter(q_a | q_b) | ||||
|  | ||||
|         return queryset.filter(q_a | q_b).distinct() | ||||
|  | ||||
|  | ||||
| class BomMixin: | ||||
|   | ||||
| @@ -405,10 +405,9 @@ class StockFilter(rest_filters.FilterSet): | ||||
|  | ||||
|         if str2bool(value): | ||||
|             # Filter StockItem with either build allocations or sales order allocations | ||||
|             return queryset.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False)) | ||||
|         else: | ||||
|             # Filter StockItem without build allocations or sales order allocations | ||||
|             return queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) | ||||
|             return queryset.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False)).distinct() | ||||
|         # Filter StockItem without build allocations or sales order allocations | ||||
|         return queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) | ||||
|  | ||||
|     expired = rest_filters.BooleanFilter(label='Expired', method='filter_expired') | ||||
|  | ||||
| @@ -476,8 +475,8 @@ class StockFilter(rest_filters.FilterSet): | ||||
|  | ||||
|         if str2bool(value): | ||||
|             return queryset.exclude(q) | ||||
|         else: | ||||
|             return queryset.filter(q) | ||||
|  | ||||
|         return queryset.filter(q).distinct() | ||||
|  | ||||
|     has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch') | ||||
|  | ||||
| @@ -487,8 +486,8 @@ class StockFilter(rest_filters.FilterSet): | ||||
|  | ||||
|         if str2bool(value): | ||||
|             return queryset.exclude(q) | ||||
|         else: | ||||
|             return queryset.filter(q) | ||||
|  | ||||
|         return queryset.filter(q).distinct() | ||||
|  | ||||
|     tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked') | ||||
|  | ||||
| @@ -504,8 +503,8 @@ class StockFilter(rest_filters.FilterSet): | ||||
|  | ||||
|         if str2bool(value): | ||||
|             return queryset.exclude(q_batch & q_serial) | ||||
|         else: | ||||
|             return queryset.filter(q_batch & q_serial) | ||||
|  | ||||
|         return queryset.filter(q_batch).filter(q_serial).distinct() | ||||
|  | ||||
|     installed = rest_filters.BooleanFilter(label='Installed in other stock item', method='filter_installed') | ||||
|  | ||||
| @@ -996,7 +995,9 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): | ||||
|         company = params.get('company', None) | ||||
|  | ||||
|         if company is not None: | ||||
|             queryset = queryset.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company)) | ||||
|             queryset = queryset.filter( | ||||
|                 Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company).distinct() | ||||
|             ) | ||||
|  | ||||
|         return queryset | ||||
|  | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import tablib | ||||
| from djmoney.money import Money | ||||
| from rest_framework import status | ||||
|  | ||||
| import build.models | ||||
| import company.models | ||||
| import part.models | ||||
| from common.models import InvenTreeSetting | ||||
| @@ -557,6 +558,100 @@ class StockItemListTest(StockAPITestCase): | ||||
|  | ||||
|         self.assertEqual(len(dataset), 17) | ||||
|  | ||||
|     def test_filter_by_allocated(self): | ||||
|         """Test that we can filter by "allocated" status: | ||||
|  | ||||
|         - Only return stock items which are 'allocated' | ||||
|         - Either to a build order or sales order | ||||
|         - Test that the results are "distinct" (no duplicated results) | ||||
|         - Ref: https://github.com/inventree/InvenTree/pull/5916 | ||||
|         """ | ||||
|  | ||||
|         # Create a build order to allocate to | ||||
|         assembly = part.models.Part.objects.create(name='F Assembly', description='Assembly for filter test', assembly=True) | ||||
|         component = part.models.Part.objects.create(name='F Component', description='Component for filter test', component=True) | ||||
|         bom_item = part.models.BomItem.objects.create(part=assembly, sub_part=component, quantity=10) | ||||
|  | ||||
|         # Create two build orders | ||||
|         bo_1 = build.models.Build.objects.create(part=assembly, quantity=10) | ||||
|         bo_2 = build.models.Build.objects.create(part=assembly, quantity=20) | ||||
|  | ||||
|         # Test that two distinct build line items are created automatically | ||||
|         self.assertEqual(bo_1.build_lines.count(), 1) | ||||
|         self.assertEqual(bo_2.build_lines.count(), 1) | ||||
|         self.assertEqual(build.models.BuildLine.objects.filter(bom_item=bom_item).count(), 2) | ||||
|  | ||||
|         build_line_1 = bo_1.build_lines.first() | ||||
|         build_line_2 = bo_2.build_lines.first() | ||||
|  | ||||
|         # Allocate stock | ||||
|         location = StockLocation.objects.first() | ||||
|         stock_1 = StockItem.objects.create(part=component, quantity=100, location=location) | ||||
|         stock_2 = StockItem.objects.create(part=component, quantity=100, location=location) | ||||
|         stock_3 = StockItem.objects.create(part=component, quantity=100, location=location) | ||||
|  | ||||
|         # Allocate stock_1 to two build orders | ||||
|         build.models.BuildItem.objects.create( | ||||
|             stock_item=stock_1, | ||||
|             build_line=build_line_1, | ||||
|             quantity=5 | ||||
|         ) | ||||
|  | ||||
|         build.models.BuildItem.objects.create( | ||||
|             stock_item=stock_1, | ||||
|             build_line=build_line_2, | ||||
|             quantity=5 | ||||
|         ) | ||||
|  | ||||
|         # Allocate stock_2 to 1 build orders | ||||
|         build.models.BuildItem.objects.create( | ||||
|             stock_item=stock_2, | ||||
|             build_line=build_line_1, | ||||
|             quantity=5 | ||||
|         ) | ||||
|  | ||||
|         url = reverse('api-stock-list') | ||||
|  | ||||
|         # 3 items when just filtering by part | ||||
|         response = self.get( | ||||
|             url, | ||||
|             { | ||||
|                 "part": component.pk, | ||||
|                 "in_stock": True | ||||
|             }, | ||||
|             expected_code=200 | ||||
|         ) | ||||
|         self.assertEqual(len(response.data), 3) | ||||
|  | ||||
|         # 1 item when filtering by "not allocated" | ||||
|         response = self.get( | ||||
|             url, | ||||
|             { | ||||
|                 "part": component.pk, | ||||
|                 "in_stock": True, | ||||
|                 "allocated": False, | ||||
|             }, | ||||
|             expected_code=200 | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(len(response.data), 1) | ||||
|         self.assertEqual(response.data[0]["pk"], stock_3.pk) | ||||
|  | ||||
|         # 2 items when filtering by "allocated" | ||||
|         response = self.get( | ||||
|             url, | ||||
|             { | ||||
|                 "part": component.pk, | ||||
|                 "in_stock": True, | ||||
|                 "allocated": True, | ||||
|             }, | ||||
|             expected_code=200 | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(len(response.data), 2) | ||||
|         self.assertEqual(response.data[0]["pk"], stock_1.pk) | ||||
|         self.assertEqual(response.data[1]["pk"], stock_2.pk) | ||||
|  | ||||
|     def test_query_count(self): | ||||
|         """Test that the number of queries required to fetch stock items is reasonable.""" | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user