Repository: saleor/saleor

Issue:
"'App' object has no attribute 'is_anonymous'" on creating orderfulfillment
### What I'm trying to achieve
I'm using an app user (serviceaccount ) with permissions manage_orders and manage_shipment.
I'm trying to create a fulfillment: 
```
mutation createFulfillment($orderId: ID!, $input: OrderFulfillInput!) {
  orderFulfill(order: $orderId, input: $input) {
    fulfillments {
      id
      status
    }
  }
}
```
attributes:
```
{
  "orderId": "T3JkZXI6Ng==" ,
  "input": { "lines": [{ "orderLineId": "T3JkZXJMaW5lOjU=", "stocks": [{  "quantity": 1, "warehouse": "V2FyZWhvdXNlOmI5ZTRlMjQzLWY0M2EtNDZkNy05NWZjLWE3NTY2ODUzMzE5OQ==" }] }] }
}
``` 

I receive the following message back: 
```
{
  "errors": [
    {
      "message": "'App' object has no attribute 'is_anonymous'",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "orderFulfill"
      ],
      "extensions": {
        "exception": {
          "code": "AttributeError"
        }
      }
    }
  ],
  "data": {
    "orderFulfill": null
  }
}
```

I'm using saleor 2.10 stable.

logs: 
```
{
  "errors": [
    {
      "message": "'App' object has no attribute 'is_anonymous'",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "orderFulfill"
      ],
      "extensions": {
        "exception": {
          "code": "AttributeError"
        }
      }
    }
  ],
  "data": {
    "orderFulfill": null
  }
}
```

### Steps to reproduce the problem
1. Create a serviceaccount with the permissions as above
2. Try to create a fulfillment for an order

### What I expected to happen
…

### Screenshots


**System information**
Operating system:



Discussion:
Hi @JannikZed 
Thanks for catching and reporting this.
The main issue is that `Events` doesn't support requests from `App`/`ServiceAccount`. There is an issue for this - https://github.com/mirumee/saleor/issues/5274.
After we rebuild the fulfillment to use more advanced warehouse system, we didn't discover that for `OrderFulfillment` we pass `User` **or** `App/ServiceAccount` and this raises an error.



Resolving patch:
diff --git a/saleor/graphql/order/bulk_mutations/orders.py b/saleor/graphql/order/bulk_mutations/orders.py
--- a/saleor/graphql/order/bulk_mutations/orders.py
+++ b/saleor/graphql/order/bulk_mutations/orders.py
@@ -5,7 +5,6 @@
 from ....order.actions import cancel_order
 from ...core.mutations import BaseBulkMutation
 from ...core.types.common import OrderError
-from ...utils import get_user_or_app_from_context
 from ..mutations.orders import clean_order_cancel
 
 
@@ -28,7 +27,7 @@ def clean_instance(cls, info, instance):
 
     @classmethod
     def perform_mutation(cls, root, info, ids, **data):
-        data["user"] = get_user_or_app_from_context(info.context)
+        data["user"] = info.context.user
         return super().perform_mutation(root, info, ids, **data)
 
     @classmethod
diff --git a/saleor/graphql/order/mutations/fulfillments.py b/saleor/graphql/order/mutations/fulfillments.py
--- a/saleor/graphql/order/mutations/fulfillments.py
+++ b/saleor/graphql/order/mutations/fulfillments.py
@@ -19,7 +19,6 @@
 from ...core.utils import from_global_id_strict_type, get_duplicated_values
 from ...meta.deprecated.mutations import ClearMetaBaseMutation, UpdateMetaBaseMutation
 from ...order.types import Fulfillment, Order
-from ...utils import get_user_or_app_from_context
 from ...warehouse.types import Warehouse
 from ..types import OrderLine
 
@@ -236,13 +235,13 @@ def perform_mutation(cls, _root, info, order, **data):
 
         cleaned_input = cls.clean_input(data)
 
-        requester = get_user_or_app_from_context(info.context)
+        user = info.context.user
         lines_for_warehouses = cleaned_input["lines_for_warehouses"]
         notify_customer = cleaned_input.get("notify_customer", True)
 
         try:
             fulfillments = create_fulfillments(
-                requester, order, dict(lines_for_warehouses), notify_customer
+                user, order, dict(lines_for_warehouses), notify_customer
             )
         except InsufficientStock as exc:
             order_line_global_id = graphene.Node.to_global_id(
diff --git a/saleor/graphql/order/mutations/orders.py b/saleor/graphql/order/mutations/orders.py
--- a/saleor/graphql/order/mutations/orders.py
+++ b/saleor/graphql/order/mutations/orders.py
@@ -26,7 +26,6 @@
 from ...order.mutations.draft_orders import DraftOrderUpdate
 from ...order.types import Order, OrderEvent
 from ...shipping.types import ShippingMethod
-from ...utils import get_user_or_app_from_context
 
 
 def clean_order_update_shipping(order, method):
@@ -323,8 +322,7 @@ class Meta:
     def perform_mutation(cls, _root, info, **data):
         order = cls.get_node_or_error(info, data.get("id"), only_type=Order)
         clean_order_cancel(order)
-        requester = get_user_or_app_from_context(info.context)
-        cancel_order(order=order, user=requester)
+        cancel_order(order=order, user=info.context.user)
         return OrderCancel(order=order)
 
 


Test patch:
diff --git a/tests/api/test_fulfillment.py b/tests/api/test_fulfillment.py
--- a/tests/api/test_fulfillment.py
+++ b/tests/api/test_fulfillment.py
@@ -2,6 +2,7 @@
 
 import graphene
 import pytest
+from django.contrib.auth.models import AnonymousUser
 
 from saleor.core.exceptions import InsufficientStock
 from saleor.core.permissions import OrderPermissions
@@ -82,6 +83,56 @@ def test_order_fulfill(
     )
 
 
+@patch("saleor.graphql.order.mutations.fulfillments.create_fulfillments")
+def test_order_fulfill_as_app(
+    mock_create_fulfillments,
+    app_api_client,
+    staff_user,
+    order_with_lines,
+    permission_manage_orders,
+    warehouse,
+):
+    order = order_with_lines
+    query = ORDER_FULFILL_QUERY
+    order_id = graphene.Node.to_global_id("Order", order.id)
+    order_line, order_line2 = order.lines.all()
+    order_line_id = graphene.Node.to_global_id("OrderLine", order_line.id)
+    order_line2_id = graphene.Node.to_global_id("OrderLine", order_line2.id)
+    warehouse_id = graphene.Node.to_global_id("Warehouse", warehouse.pk)
+    variables = {
+        "order": order_id,
+        "input": {
+            "notifyCustomer": True,
+            "lines": [
+                {
+                    "orderLineId": order_line_id,
+                    "stocks": [{"quantity": 3, "warehouse": warehouse_id}],
+                },
+                {
+                    "orderLineId": order_line2_id,
+                    "stocks": [{"quantity": 2, "warehouse": warehouse_id}],
+                },
+            ],
+        },
+    }
+    response = app_api_client.post_graphql(
+        query, variables, permissions=[permission_manage_orders]
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["orderFulfill"]
+    assert not data["orderErrors"]
+
+    fulfillment_lines_for_warehouses = {
+        str(warehouse.pk): [
+            {"order_line": order_line, "quantity": 3},
+            {"order_line": order_line2, "quantity": 2},
+        ]
+    }
+    mock_create_fulfillments.assert_called_once_with(
+        AnonymousUser(), order, fulfillment_lines_for_warehouses, True
+    )
+
+
 @patch("saleor.graphql.order.mutations.fulfillments.create_fulfillments")
 def test_order_fulfill_many_warehouses(
     mock_create_fulfillments,
diff --git a/tests/api/test_order.py b/tests/api/test_order.py
--- a/tests/api/test_order.py
+++ b/tests/api/test_order.py
@@ -4,6 +4,7 @@
 
 import graphene
 import pytest
+from django.contrib.auth.models import AnonymousUser
 from django.core.exceptions import ValidationError
 from freezegun import freeze_time
 from prices import Money, TaxedMoney
@@ -1465,6 +1466,21 @@ def test_order_add_note_fail_on_empty_message(
     assert data["orderErrors"][0]["code"] == OrderErrorCode.REQUIRED.name
 
 
+MUTATION_ORDER_CANCEL = """
+mutation cancelOrder($id: ID!) {
+    orderCancel(id: $id) {
+        order {
+            status
+        }
+        orderErrors{
+            field
+            code
+        }
+    }
+}
+"""
+
+
 @patch("saleor.graphql.order.mutations.orders.cancel_order")
 @patch("saleor.graphql.order.mutations.orders.clean_order_cancel")
 def test_order_cancel(
@@ -1475,23 +1491,10 @@ def test_order_cancel(
     order_with_lines,
 ):
     order = order_with_lines
-    query = """
-        mutation cancelOrder($id: ID!) {
-            orderCancel(id: $id) {
-                order {
-                    status
-                }
-                orderErrors{
-                    field
-                    code
-                }
-            }
-        }
-    """
     order_id = graphene.Node.to_global_id("Order", order.id)
     variables = {"id": order_id}
     response = staff_api_client.post_graphql(
-        query, variables, permissions=[permission_manage_orders]
+        MUTATION_ORDER_CANCEL, variables, permissions=[permission_manage_orders]
     )
     content = get_graphql_content(response)
     data = content["data"]["orderCancel"]
@@ -1501,6 +1504,29 @@ def test_order_cancel(
     mock_cancel_order.assert_called_once_with(order=order, user=staff_api_client.user)
 
 
+@patch("saleor.graphql.order.mutations.orders.cancel_order")
+@patch("saleor.graphql.order.mutations.orders.clean_order_cancel")
+def test_order_cancel_as_app(
+    mock_clean_order_cancel,
+    mock_cancel_order,
+    app_api_client,
+    permission_manage_orders,
+    order_with_lines,
+):
+    order = order_with_lines
+    order_id = graphene.Node.to_global_id("Order", order.id)
+    variables = {"id": order_id}
+    response = app_api_client.post_graphql(
+        MUTATION_ORDER_CANCEL, variables, permissions=[permission_manage_orders]
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["orderCancel"]
+    assert not data["orderErrors"]
+
+    mock_clean_order_cancel.assert_called_once_with(order)
+    mock_cancel_order.assert_called_once_with(order=order, user=AnonymousUser())
+
+
 def test_order_capture(
     staff_api_client, permission_manage_orders, payment_txn_preauth, staff_user
 ):
@@ -2150,6 +2176,19 @@ def test_query_draft_order_by_token_as_anonymous_customer(api_client, draft_orde
     assert not content["data"]["orderByToken"]
 
 
+MUTATION_ORDER_BULK_CANCEL = """
+mutation CancelManyOrders($ids: [ID]!) {
+    orderBulkCancel(ids: $ids) {
+        count
+        orderErrors{
+            field
+            code
+        }
+    }
+}
+"""
+
+
 @patch("saleor.graphql.order.bulk_mutations.orders.cancel_order")
 def test_order_bulk_cancel(
     mock_cancel_order,
@@ -2159,17 +2198,6 @@ def test_order_bulk_cancel(
     permission_manage_orders,
     address,
 ):
-    query = """
-        mutation CancelManyOrders($ids: [ID]!) {
-            orderBulkCancel(ids: $ids) {
-                count
-                orderErrors{
-                    field
-                    code
-                }
-            }
-        }
-    """
     orders = order_list
     orders.append(fulfilled_order_with_all_cancelled_fulfillments)
     expected_count = sum(order.can_cancel() for order in orders)
@@ -2177,7 +2205,7 @@ def test_order_bulk_cancel(
         "ids": [graphene.Node.to_global_id("Order", order.id) for order in orders],
     }
     response = staff_api_client.post_graphql(
-        query, variables, permissions=[permission_manage_orders]
+        MUTATION_ORDER_BULK_CANCEL, variables, permissions=[permission_manage_orders]
     )
     content = get_graphql_content(response)
     data = content["data"]["orderBulkCancel"]
@@ -2190,6 +2218,35 @@ def test_order_bulk_cancel(
     mock_cancel_order.call_count == expected_count
 
 
+@patch("saleor.graphql.order.bulk_mutations.orders.cancel_order")
+def test_order_bulk_cancel_as_app(
+    mock_cancel_order,
+    app_api_client,
+    order_list,
+    fulfilled_order_with_all_cancelled_fulfillments,
+    permission_manage_orders,
+    address,
+):
+    orders = order_list
+    orders.append(fulfilled_order_with_all_cancelled_fulfillments)
+    expected_count = sum(order.can_cancel() for order in orders)
+    variables = {
+        "ids": [graphene.Node.to_global_id("Order", order.id) for order in orders],
+    }
+    response = app_api_client.post_graphql(
+        MUTATION_ORDER_BULK_CANCEL, variables, permissions=[permission_manage_orders]
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["orderBulkCancel"]
+    assert data["count"] == expected_count
+    assert not data["orderErrors"]
+
+    calls = [call(order=order, user=AnonymousUser()) for order in orders]
+
+    mock_cancel_order.assert_has_calls(calls, any_order=True)
+    mock_cancel_order.call_count == expected_count
+
+
 @pytest.mark.parametrize(
     "orders_filter, count",
     [