Repository: saleor/saleor

Issue:
Unique validation SKU when default variant create
In pull request #3505, added default variant create to Product mutations. We should validate if SKU is unique.


Discussion:

Resolving patch:
diff --git a/saleor/graphql/product/mutations/products.py b/saleor/graphql/product/mutations/products.py
--- a/saleor/graphql/product/mutations/products.py
+++ b/saleor/graphql/product/mutations/products.py
@@ -336,8 +336,13 @@ def clean_sku(cls, product_type, cleaned_input, errors):
         See the documentation for `has_variants` field for details:
         http://docs.getsaleor.com/en/latest/architecture/products.html#product-types
         """
-        if not product_type.has_variants and not cleaned_input.get('sku'):
-            cls.add_error(errors, 'sku', 'This field cannot be blank.')
+        if not product_type.has_variants:
+            input_sku = cleaned_input.get('sku')
+            if not input_sku:
+                cls.add_error(errors, 'sku', 'This field cannot be blank.')
+            elif models.ProductVariant.objects.filter(sku=input_sku).exists():
+                cls.add_error(
+                    errors, 'sku', 'Product with this SKU already exists.')
 
     @classmethod
     @transaction.atomic
@@ -377,9 +382,12 @@ class Meta:
 
     @classmethod
     def clean_sku(cls, product_type, cleaned_input, errors):
-        # SKU is an optional field in the product update mutation,
-        # so we explicitly skip the validation.
-        pass
+        input_sku = cleaned_input.get('sku')
+        if (not product_type.has_variants and
+                input_sku and
+                models.ProductVariant.objects.filter(sku=input_sku).exists()):
+            cls.add_error(
+                errors, 'sku', 'Product with this SKU already exists.')
 
     @classmethod
     @transaction.atomic


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
@@ -599,6 +599,41 @@ def test_create_product_without_variants_sku_validation(
     assert data['errors'][0]['message'] == 'This field cannot be blank.'
 
 
+def test_create_product_without_variants_sku_duplication(
+        staff_api_client, product_type_without_variant, category,
+        permission_manage_products, product_with_default_variant):
+    query = QUERY_CREATE_PRODUCT_WITHOUT_VARIANTS
+
+    product_type = product_type_without_variant
+    product_type_id = graphene.Node.to_global_id(
+        'ProductType', product_type.pk)
+    category_id = graphene.Node.to_global_id(
+        'Category', category.pk)
+    product_name = 'test name'
+    product_description = 'description'
+    product_price = 10
+    quantity = 1
+    track_inventory = True
+    sku = '1234'
+
+    variables = {
+        'productTypeId': product_type_id,
+        'categoryId': category_id,
+        'name': product_name,
+        'description': product_description,
+        'price': product_price,
+        'sku': sku,
+        'quantity': quantity,
+        'trackInventory': track_inventory}
+
+    response = staff_api_client.post_graphql(
+        query, variables, permissions=[permission_manage_products])
+    content = get_graphql_content(response)
+    data = content['data']['productCreate']
+    assert data['errors'][0]['field'] == 'sku'
+    assert data['errors'][0]['message'] == 'Product with this SKU already exists.'
+
+
 def test_update_product(
         staff_api_client, category, non_default_category, product,
         permission_manage_products):
@@ -751,6 +786,45 @@ def test_update_product_without_variants(
     assert product['trackInventory'] == product_track_inventory
 
 
+def test_update_product_without_variants_sku_duplication(
+        staff_api_client, product_with_default_variant,
+        permission_manage_products, product):
+    query = """
+    mutation updateProduct(
+        $productId: ID!,
+        $sku: String)
+    {
+        productUpdate(
+            id: $productId,
+            input: {
+                sku: $sku
+            })
+        {
+            product {
+                id
+            }
+            errors {
+                message
+                field
+            }
+        }
+    }"""
+    product = product_with_default_variant
+    product_id = graphene.Node.to_global_id('Product', product.pk)
+    product_sku = "123"
+
+    variables = {
+        'productId': product_id,
+        'sku': product_sku}
+
+    response = staff_api_client.post_graphql(
+        query, variables, permissions=[permission_manage_products])
+    content = get_graphql_content(response)
+    data = content['data']['productUpdate']
+    assert data['errors']
+    assert data['errors'][0]['field'] == 'sku'
+    assert data['errors'][0]['message'] == 'Product with this SKU already exists.'
+
 def test_delete_product(staff_api_client, product, permission_manage_products):
     query = """
         mutation DeleteProduct($id: ID!) {
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -329,7 +329,7 @@ def product_with_default_variant(product_type_without_variant, category):
         name='Test product', price=Money('10.00', 'USD'),
         product_type=product_type_without_variant, category=category)
     ProductVariant.objects.create(
-        product=product, sku='123', track_inventory=True,
+        product=product, sku='1234', track_inventory=True,
         quantity=100)
     return product