Repository: saleor/saleor
Pull request: Create product without category
I want to merge this change because...closes https://github.com/mirumee/saleor/issues/5589

<!-- Please mention all relevant issue numbers. -->

# Impact

* [ ] New migrations
* [ ] New/Updated API fields or mutations
* [ ] Deprecated API fields or mutations
* [ ] Removed API types, fields, or mutations

# Pull Request Checklist

<!-- Please keep this section. It will make maintainer's life easier. -->

* [X] Privileged queries and mutations are guarded by proper permission checks
* [X] Database queries are optimized and the number of queries is constant
* [X] Database migration files are up to date
* [X] The changes are tested
* [X] GraphQL schema and type definitions are up to date
* [X] Changes are mentioned in the changelog


Problem statement:
Cannot create product without assigning it to category
### What I'm trying to achieve
To create product with "category" field not filled

### Steps to reproduce the problem
1. Go to dashboard
2. Fill all fields except category
3. Get this
```
{
  "errors": [
    {
      "message": "The loader.load() function must be called with a value,but got: None.",
      "locations": [
        {
          "line": 82,
          "column": 3
        }
      ],
      "path": [
        "productCreate",
        "product",
        "category"
      ],
      "extensions": {
        "exception": {
          "code": "TypeError",
          "stacktrace": [
            "Traceback (most recent call last):",
            "  File \"/Users/anders/.pyenv/versions/saleor/lib/python3.8/site-packages/promise/promise.py\", line 489, in _resolve_from_executor",
            "    executor(resolve, reject)",
            "  File \"/Users/anders/.pyenv/versions/saleor/lib/python3.8/site-packages/promise/promise.py\", line 756, in executor",
            "    return resolve(f(*args, **kwargs))",
            "  File \"/Users/anders/.pyenv/versions/saleor/lib/python3.8/site-packages/graphql/execution/middleware.py\", line 75, in make_it_promise",
            "    return next(*args, **kwargs)",
            "  File \"/Users/anders/projects/saleor/saleor/graphql/product/types/products.py\", line 462, in resolve_category",
            "    return CategoryByIdLoader(info.context).load(root.category_id)",
            "  File \"/Users/anders/.pyenv/versions/saleor/lib/python3.8/site-packages/promise/dataloader.py\", line 86, in load",
            "    raise TypeError(",
            "TypeError: The loader.load() function must be called with a value,but got: None."
          ]
        }
      }
    }
  ],
  "data": {
    "productCreate": {
      "errors": [],
      "product": {
        "id": "UHJvZHVjdDoxMTk=",
        "attributes": [
          {
            "attribute": {
              "id": "QXR0cmlidXRlOjIx",
              "slug": "abv",
              "name": "ABV",
              "inputType": "DROPDOWN",
              "valueRequired": false,
              "values": [
                {
                  "id": "QXR0cmlidXRlVmFsdWU6Njg=",
                  "name": "5.1%",
                  "slug": "51",
                  "__typename": "AttributeValue"
                },
                {
                  "id": "QXR0cmlidXRlVmFsdWU6Njk=",
                  "name": "6.7%",
                  "slug": "67",
                  "__typename": "AttributeValue"
                },
                {
                  "id": "QXR0cmlidXRlVmFsdWU6ODI=",
                  "name": "1%",
                  "slug": "1",
                  "__typename": "AttributeValue"
                }
              ],
              "__typename": "Attribute"
            },
            "values": [
              {
                "id": "QXR0cmlidXRlVmFsdWU6ODI=",
                "name": "1%",
                "slug": "1",
                "__typename": "AttributeValue"
              }
            ],
            "__typename": "SelectedAttribute"
          }
        ],
        "productType": {
          "id": "UHJvZHVjdFR5cGU6MTE=",
          "variantAttributes": [],
          "__typename": "ProductType",
          "name": "Beer",
          "hasVariants": false
        },
        "__typename": "Product",
        "name": "piwko",
        "descriptionJson": "{}",
        "seoTitle": "",
        "seoDescription": "",
        "category": null,
        "collections": [],
        "basePrice": {
          "amount": 0,
          "currency": "USD",
          "__typename": "Money"
        },
        "margin": {
          "start": 0,
          "stop": 0,
          "__typename": "Margin"
        },
        "purchaseCost": {
          "start": {
            "amount": 0,
            "currency": "USD",
            "__typename": "Money"
          },
          "stop": {
            "amount": 0,
            "currency": "USD",
            "__typename": "Money"
          },
          "__typename": "MoneyRange"
        },
        "isAvailable": false,
        "isPublished": false,
        "chargeTaxes": false,
        "publicationDate": null,
        "pricing": {
          "priceRange": {
            "start": {
              "net": {
                "amount": 0,
                "currency": "USD",
                "__typename": "Money"
              },
              "__typename": "TaxedMoney"
            },
            "stop": {
              "net": {
                "amount": 0,
                "currency": "USD",
                "__typename": "Money"
              },
              "__typename": "TaxedMoney"
            },
            "__typename": "TaxedMoneyRange"
          },
          "__typename": "ProductPricingInfo"
        },
        "images": [],
        "variants": [
          {
            "id": "UHJvZHVjdFZhcmlhbnQ6MzE2",
            "sku": "3123",
            "name": "",
            "priceOverride": null,
            "margin": null,
            "stocks": [
              {
                "id": "U3RvY2s6NjI3",
                "quantity": 9,
                "quantityAllocated": 0,
                "warehouse": {
                  "id": "V2FyZWhvdXNlOmUyZjAyNDlmLTc1MzEtNDU2Ny1hODExLTM4NmY4ZGJkNzlkNQ==",
                  "name": "Americas",
                  "__typename": "Warehouse"
                },
                "__typename": "Stock"
              }
            ],
            "trackInventory": false,
            "__typename": "ProductVariant"
          }
        ]
      },
      "__typename": "ProductCreate"
    }
  }
}
```

Hints from issue discussion:

Commit message: Fix creating product without category

Patch to review:
diff --git a/saleor/graphql/product/types/products.py b/saleor/graphql/product/types/products.py
index 19b8548d151..077275b49af 100644
--- a/saleor/graphql/product/types/products.py
+++ b/saleor/graphql/product/types/products.py
@@ -460,7 +460,10 @@ class Meta:
 
     @staticmethod
     def resolve_category(root: models.Product, info):
-        return CategoryByIdLoader(info.context).load(root.category_id)
+        category_id = root.category_id
+        if category_id:
+            return CategoryByIdLoader(info.context).load(root.category_id)
+        return None
 
     @staticmethod
     def resolve_tax_type(root: models.Product, info):
diff --git a/tests/api/test_product.py b/tests/api/test_product.py
index a762915bfc7..11f02301b41 100644
--- a/tests/api/test_product.py
+++ b/tests/api/test_product.py
@@ -1054,6 +1054,57 @@ def test_create_product_no_slug_in_input(
     assert str(data["product"]["basePrice"]["amount"]) == product_price
 
 
+def test_create_product_no_category_id(
+    staff_api_client,
+    product_type,
+    category,
+    size_attribute,
+    description_json,
+    permission_manage_products,
+    monkeypatch,
+):
+    query = CREATE_PRODUCT_MUTATION
+
+    product_type_id = graphene.Node.to_global_id("ProductType", product_type.pk)
+    product_name = "test name"
+    product_is_published = False
+    product_tax_rate = "STANDARD"
+    product_price = "22.33"
+    input_slug = "test-slug"
+
+    # Mock tax interface with fake response from tax gateway
+    monkeypatch.setattr(
+        PluginsManager,
+        "get_tax_code_from_object_meta",
+        lambda self, x: TaxType(description="", code=product_tax_rate),
+    )
+
+    variables = {
+        "input": {
+            "productType": product_type_id,
+            "name": product_name,
+            "slug": input_slug,
+            "isPublished": product_is_published,
+            "taxCode": product_tax_rate,
+            "basePrice": product_price,
+        }
+    }
+
+    response = staff_api_client.post_graphql(
+        query, variables, permissions=[permission_manage_products]
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["productCreate"]
+    assert data["errors"] == []
+    assert data["product"]["name"] == product_name
+    assert data["product"]["slug"] == input_slug
+    assert data["product"]["isPublished"] == product_is_published
+    assert data["product"]["taxType"]["taxCode"] == product_tax_rate
+    assert data["product"]["productType"]["name"] == product_type.name
+    assert data["product"]["category"] is None
+    assert str(data["product"]["basePrice"]["amount"]) == product_price
+
+
 def test_create_product_with_negative_weight(
     staff_api_client,
     product_type,


Code review comments:
diff_hunk: @@ -460,7 +460,10 @@ class Meta:
 
     @staticmethod
     def resolve_category(root: models.Product, info):
-        return CategoryByIdLoader(info.context).load(root.category_id)
+        category_id = root.category_id
+        if category_id:
+            return CategoryByIdLoader(info.context).load(root.category_id)
path: saleor/graphql/product/types/products.py
text: @user1:
```suggestion
            return CategoryByIdLoader(info.context).load(category_id)
```