Repository: saleor/saleor

Issue:
Add multiple product types filter to products
### What I'm trying to achieve
To be able to filter products by multiple types.

### Describe a proposed solution
```
input ProductFilterInput {
  isPublished: Boolean
  collections: [ID]
  categories: [ID]
  hasCategory: Boolean
  price: PriceRangeInput
  attributes: [AttributeInput]
  stockAvailability: StockAvailability
+ productType: ID @deprecated
+ productTypes: [ID]
  search: String
  minimalPrice: PriceRangeInput
}
```

It would also require to make this change
```
input ProductTypeFilterInput {
  search: String
  configurable: ProductTypeConfigurable
  productType: ProductTypeEnum
+ ids: [ID]
}
```



Discussion:

Resolving patch:
diff --git a/saleor/graphql/product/filters.py b/saleor/graphql/product/filters.py
--- a/saleor/graphql/product/filters.py
+++ b/saleor/graphql/product/filters.py
@@ -47,15 +47,18 @@ def _clean_product_attributes_filter_input(
         for attr in attributes
     }
     queries: Dict[int, List[Optional[int]]] = defaultdict(list)
-
     # Convert attribute:value pairs into a dictionary where
     # attributes are keys and values are grouped in lists
-    for attr_name, val_slug in filter_value:
+    for attr_name, val_slugs in filter_value:
         if attr_name not in attributes_map:
             raise ValueError("Unknown attribute name: %r" % (attr_name,))
         attr_pk = attributes_map[attr_name]
-        attr_val_pk = values_map[attr_name].get(val_slug)
-        queries[attr_pk].append(attr_val_pk)
+        attr_val_pk = [
+            values_map[attr_name][val_slug]
+            for val_slug in val_slugs
+            if val_slug in values_map[attr_name]
+        ]
+        queries[attr_pk] += attr_val_pk
 
     return queries
 
@@ -113,8 +116,12 @@ def filter_products_by_stock_availability(qs, stock_availability):
 
 def filter_attributes(qs, _, value):
     if value:
-        value = [(v["slug"], v["value"]) for v in value]
-        qs = filter_products_by_attributes(qs, value)
+        value_list = []
+        for v in value:
+            slug = v["slug"]
+            values = [v["value"]] if "value" in v else v.get("values", [])
+            value_list.append((slug, values))
+        qs = filter_products_by_attributes(qs, value_list)
     return qs
 
 
@@ -237,7 +244,8 @@ class ProductFilter(django_filters.FilterSet):
     stock_availability = EnumFilter(
         input_class=StockAvailability, method=filter_stock_availability
     )
-    product_type = GlobalIDFilter()
+    product_type = GlobalIDFilter()  # Deprecated
+    product_types = GlobalIDMultipleChoiceFilter(field_name="product_type")
     search = django_filters.CharFilter(method=filter_search)
 
     class Meta:
@@ -288,6 +296,7 @@ class ProductTypeFilter(django_filters.FilterSet):
     )
 
     product_type = EnumFilter(input_class=ProductTypeEnum, method=filter_product_type)
+    ids = GlobalIDMultipleChoiceFilter(field_name="id")
 
     class Meta:
         model = ProductType
diff --git a/saleor/graphql/product/types/attributes.py b/saleor/graphql/product/types/attributes.py
--- a/saleor/graphql/product/types/attributes.py
+++ b/saleor/graphql/product/types/attributes.py
@@ -161,4 +161,13 @@ class Meta:
 
 class AttributeInput(graphene.InputObjectType):
     slug = graphene.String(required=True, description=AttributeDescriptions.SLUG)
-    value = graphene.String(required=True, description=AttributeValueDescriptions.SLUG)
+    value = graphene.String(
+        required=False,
+        description=(
+            "Internal representation of a value (unique per attribute). "
+            "DEPRECATED: Will be removed in Saleor 2.11"
+        ),
+    )  # deprecated
+    values = graphene.List(
+        graphene.String, required=False, description=AttributeValueDescriptions.SLUG
+    )
diff --git a/saleor/graphql/product/types/products.py b/saleor/graphql/product/types/products.py
--- a/saleor/graphql/product/types/products.py
+++ b/saleor/graphql/product/types/products.py
@@ -280,14 +280,7 @@ class Meta:
         description = (
             "Represents a version of a product such as different size or color."
         )
-        only_fields = [
-            "id",
-            "name",
-            "product",
-            "sku",
-            "track_inventory",
-            "weight",
-        ]
+        only_fields = ["id", "name", "product", "sku", "track_inventory", "weight"]
         interfaces = [relay.Node]
         model = models.ProductVariant
 


Test patch:
diff --git a/tests/api/test_product.py b/tests/api/test_product.py
--- a/tests/api/test_product.py
+++ b/tests/api/test_product.py
@@ -2951,3 +2951,49 @@ def test_filter_product_types_by_custom_search_value(
     matched_names = sorted([result["node"]["name"] for result in results])
 
     assert matched_names == sorted(expected_names)
+
+
+def test_product_filter_by_attribute_values(
+    staff_api_client,
+    permission_manage_products,
+    color_attribute,
+    pink_attribute_value,
+    product_with_variant_with_two_attributes,
+):
+    query = """
+    query Products($filters: ProductFilterInput) {
+      products(first: 5, filter: $filters) {
+        edges {
+        node {
+          id
+          name
+          attributes {
+            attribute {
+              name
+              slug
+            }
+            values {
+              name
+              slug
+            }
+          }
+        }
+        }
+      }
+    }
+    """
+    variables = {
+        "attributes": [
+            {"slug": color_attribute.slug, "values": [pink_attribute_value.slug]}
+        ]
+    }
+    response = staff_api_client.post_graphql(query, variables)
+    content = get_graphql_content(response)
+    assert not content["data"]["products"]["edges"] == [
+        {
+            "node": {
+                "attributes": [],
+                "name": product_with_variant_with_two_attributes.name,
+            }
+        }
+    ]