Repository: saleor/saleor
Pull request: Add list enum to some filters fields
I want to merge this change because... Closes #4006

1. [ ] Privileged views and APIs are guarded by proper permission checks.
1. [ ] All visible strings are translated with proper context.
1. [ ] All data-formatting is locale-aware (dates, numbers, and so on).
1. [ ] Database queries are optimized and the number of queries is constant.
1. [ ] Database migration files are up to date.
1. [ ] The changes are tested.
1. [ ] The code is documented (docstrings, project documentation).
1. [ ] GraphQL schema and type definitions are up to date.
1. [x] Changes are mentioned in the changelog.

Problem statement:
Allow filtering by lists of enums
In GraphQL API we can filter data by enum fields:
```
orders(filter: {status: READY_TO_FULFILL})
```

It would be relatively easy to make the filtering more flexible by changing enum filters to lists: 
```
orders(filter: {status: [READY_TO_CAPTURE, READY_TO_FULFILL]})
```

Hints from issue discussion:

Commit message: Update changelog

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e4f627a680..83fc902b8f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,6 +55,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Add migrations since unnecessary indexes are removed in mptt model in [django-mptt/django-mptt#578](https://github.com/django-mptt/django-mptt/pull/578) - #4071 by @jxltom
 - Update order events and order email events names in typescripts - #4089 by @jxltom
 - Migrated code style to Black. Saleor now includes configurations and tools related to use black. - #3852 by krzysztofwolski and NyanKiyoshi
+- Allow filtering by lists of enums for fields: ``Voucher.filter.status``, ``Voucher.filter.discount_type``, ``Sale.filter.status``, ``Order.filter.payment_status``, ``Order.filter.status``, ``Order.filter.custom_status`` - #4078 by @korycins
 
 
 ## 2.5.0
diff --git a/saleor/graphql/core/filters.py b/saleor/graphql/core/filters.py
index f8139610588..45012ae8509 100644
--- a/saleor/graphql/core/filters.py
+++ b/saleor/graphql/core/filters.py
@@ -5,14 +5,13 @@
 
 
 class DefaultMultipleChoiceField(MultipleChoiceField):
-    default_error_messages = {
-        "invalid_choice": _("One of the specified IDs was invalid (%(value)s)."),
-        "invalid_list": _("Enter a list of values."),
-    }
+    default_error_messages = {"invalid_list": _("Enter a list of values.")}
 
     def to_python(self, value):
         if not value:
             return []
+        if not isinstance(value, list):
+            value = [value]
         return value
 
     def validate(self, value):
diff --git a/saleor/graphql/discount/filters.py b/saleor/graphql/discount/filters.py
index dfbcbef8e89..2083384c206 100644
--- a/saleor/graphql/discount/filters.py
+++ b/saleor/graphql/discount/filters.py
@@ -1,24 +1,31 @@
 from datetime import date
+from typing import List
 
 import django_filters
+from django.db.models import Q
 
 from ...discount import DiscountValueType
-from ...discount.models import Sale, Voucher
-from ..core.filters import EnumFilter, ObjectTypeFilter
+from ...discount.models import Sale, Voucher, VoucherQueryset
+from ..core.filters import ListObjectTypeFilter, ObjectTypeFilter
 from ..core.types.common import DateRangeInput, IntRangeInput
 from ..utils import filter_by_query_param
 from .enums import DiscountStatusEnum, DiscountValueTypeEnum, VoucherDiscountType
 
 
-def filter_status(qs, _, value):
+def filter_status(
+    qs: VoucherQueryset, _, value: List[DiscountStatusEnum]
+) -> VoucherQueryset:
+    if not value:
+        return qs
+    query_objects = qs.none()
     today = date.today()
-    if value == DiscountStatusEnum.ACTIVE:
-        return qs.active(today)
-    if value == DiscountStatusEnum.EXPIRED:
-        return qs.expired(today)
-    if value == DiscountStatusEnum.SCHEDULED:
-        return qs.filter(start_date__gt=today)
-    return qs
+    if DiscountStatusEnum.ACTIVE in value:
+        query_objects |= qs.active(today)
+    if DiscountStatusEnum.EXPIRED in value:
+        query_objects |= qs.expired(today)
+    if DiscountStatusEnum.SCHEDULED in value:
+        query_objects |= qs.filter(start_date__gt=today)
+    return qs & query_objects
 
 
 def filter_times_used(qs, _, value):
@@ -31,11 +38,18 @@ def filter_times_used(qs, _, value):
     return qs
 
 
-def filter_discount_type(qs, _, value):
-    if value in [VoucherDiscountType.PERCENTAGE, VoucherDiscountType.FIXED]:
-        qs = qs.filter(discount_value_type=value)
-    elif value == VoucherDiscountType.SHIPPING:
-        qs = qs.filter(type=value)
+def filter_discount_type(
+    qs: VoucherQueryset, _, value: List[VoucherDiscountType]
+) -> VoucherQueryset:
+    if value:
+        query = Q()
+        if VoucherDiscountType.FIXED in value:
+            query |= Q(discount_value_type=VoucherDiscountType.FIXED.value)
+        if VoucherDiscountType.PERCENTAGE in value:
+            query |= Q(discount_value_type=VoucherDiscountType.PERCENTAGE.value)
+        if VoucherDiscountType.SHIPPING in value:
+            query |= Q(type=VoucherDiscountType.SHIPPING)
+        qs = qs.filter(query).distinct()
     return qs
 
 
@@ -70,10 +84,10 @@ def filter_voucher_search(qs, _, value):
 
 
 class VoucherFilter(django_filters.FilterSet):
-    status = EnumFilter(input_class=DiscountStatusEnum, method=filter_status)
+    status = ListObjectTypeFilter(input_class=DiscountStatusEnum, method=filter_status)
     times_used = ObjectTypeFilter(input_class=IntRangeInput, method=filter_times_used)
 
-    discount_type = EnumFilter(
+    discount_type = ListObjectTypeFilter(
         input_class=VoucherDiscountType, method=filter_discount_type
     )
     started = ObjectTypeFilter(input_class=DateRangeInput, method=filter_started)
@@ -85,7 +99,7 @@ class Meta:
 
 
 class SaleFilter(django_filters.FilterSet):
-    status = ObjectTypeFilter(input_class=DiscountStatusEnum, method=filter_status)
+    status = ListObjectTypeFilter(input_class=DiscountStatusEnum, method=filter_status)
     sale_type = ObjectTypeFilter(
         input_class=DiscountValueTypeEnum, method=filter_sale_type
     )
diff --git a/saleor/graphql/order/filters.py b/saleor/graphql/order/filters.py
index 7b23865001c..67898a4f0f9 100644
--- a/saleor/graphql/order/filters.py
+++ b/saleor/graphql/order/filters.py
@@ -1,7 +1,8 @@
 import django_filters
+from django.db.models import Sum
 
 from ...order.models import Order
-from ..core.filters import EnumFilter, ObjectTypeFilter
+from ..core.filters import ListObjectTypeFilter, ObjectTypeFilter
 from ..core.types.common import DateRangeInput
 from ..payment.enums import PaymentChargeStatusEnum
 from ..utils import filter_by_query_param
@@ -9,17 +10,29 @@
 
 
 def filter_payment_status(qs, _, value):
-    qs = qs.filter(payments__is_active=True, payments__charge_status=value)
+    if value:
+        qs = qs.filter(payments__is_active=True, payments__charge_status__in=value)
     return qs
 
 
 def filter_status(qs, _, value):
-    if value not in [
-        OrderStatusFilter.READY_TO_CAPTURE,
-        OrderStatusFilter.READY_TO_FULFILL,
-    ]:
-        qs = qs.filter(status=value)
-    return qs
+    query_objects = qs.none()
+
+    if value:
+        query_objects |= qs.filter(status__in=value)
+
+    if OrderStatusFilter.READY_TO_FULFILL in value:
+        # to use & between queries both of them need to have applied the same
+        # annotate
+        qs = qs.annotate(amount_paid=Sum("payments__captured_amount"))
+        query_objects |= qs.ready_to_fulfill()
+
+    if OrderStatusFilter.READY_TO_CAPTURE in value:
+        qs = qs.distinct()
+        query_objects = query_objects.distinct()
+        query_objects |= qs.ready_to_capture()
+
+    return qs & query_objects
 
 
 def filter_customer(qs, _, value):
@@ -52,10 +65,10 @@ class Meta:
 
 
 class OrderFilter(DraftOrderFilter):
-    payment_status = EnumFilter(
+    payment_status = ListObjectTypeFilter(
         input_class=PaymentChargeStatusEnum, method=filter_payment_status
     )
-    status = ObjectTypeFilter(input_class=OrderStatusFilter, method=filter_status)
+    status = ListObjectTypeFilter(input_class=OrderStatusFilter, method=filter_status)
     customer = django_filters.CharFilter(method=filter_customer)
     created = ObjectTypeFilter(input_class=DateRangeInput, method=filter_created_range)
 
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index 3c28cdb35b9..2c6e5eba0ac 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -910,6 +910,8 @@ input FulfillmentUpdateTrackingInput {
 
 enum GatewaysEnum {
   DUMMY
+  BRAINTREE
+  STRIPE
 }
 
 scalar GenericScalar
@@ -1476,8 +1478,8 @@ enum OrderEventsEnum {
 }
 
 input OrderFilterInput {
-  paymentStatus: PaymentChargeStatusEnum
-  status: OrderStatusFilter
+  paymentStatus: [PaymentChargeStatusEnum]
+  status: [OrderStatusFilter]
   customer: String
   created: DateRangeInput
 }
@@ -1526,12 +1528,12 @@ enum OrderStatus {
 }
 
 enum OrderStatusFilter {
-  READY_TO_FULFILL
-  READY_TO_CAPTURE
   UNFULFILLED
   PARTIALLY_FULFILLED
   FULFILLED
   CANCELED
+  READY_TO_FULFILL
+  READY_TO_CAPTURE
 }
 
 type OrderUpdate {
@@ -2213,7 +2215,7 @@ type SaleDelete {
 }
 
 input SaleFilterInput {
-  status: DiscountStatusEnum
+  status: [DiscountStatusEnum]
   saleType: DiscountValueTypeEnum
   started: DateRangeInput
   search: String
@@ -2785,9 +2787,9 @@ enum VoucherDiscountValueType {
 }
 
 input VoucherFilterInput {
-  status: DiscountStatusEnum
+  status: [DiscountStatusEnum]
   timesUsed: IntRangeInput
-  discountType: VoucherDiscountType
+  discountType: [VoucherDiscountType]
   started: DateRangeInput
   search: String
 }


Code review comments:
diff_hunk: @@ -910,6 +910,8 @@ input FulfillmentUpdateTrackingInput {
 
 enum GatewaysEnum {
   DUMMY
+  BRAINTREE
path: saleor/graphql/schema.graphql
text: @user1:
I think we have only DUMMY enabled by default, do the other come from your local settings?

@author:
Oh, my bad!