text: from collections import defaultdict
  
  import graphene
  from django.core.exceptions import ValidationError
  from django.db import transaction
  
  from ....product import models
  from ....product.error_codes import ProductErrorCode
  from ....product.tasks import update_product_minimal_variant_price_task
  from ....product.utils import delete_categories
  from ....product.utils.attributes import generate_name_for_variant
  from ...core.mutations import (
      BaseBulkMutation,
      BaseMutation,
      ModelBulkDeleteMutation,
      ModelMutation,
  )
  from ...core.types.common import BulkProductError, ProductError
  from ..mutations.products import (
      AttributeAssignmentMixin,
      AttributeValueInput,
      ProductVariantCreate,
      ProductVariantInput,
  )
  from ..types import ProductVariant
  from ..utils import get_used_variants_attribute_values
  
  
  class CategoryBulkDelete(ModelBulkDeleteMutation):
      class Arguments:
          ids = graphene.List(
              graphene.ID, required=True, description="List of category IDs to delete."
          )
  
      class Meta:
          description = "Deletes categories."
          model = models.Category
          permissions = ("product.manage_products",)
          error_type_class = ProductError
          error_type_field = "product_errors"
  
      @classmethod
      def bulk_action(cls, queryset):
          delete_categories(queryset.values_list("pk", flat=True))
  
  
  class CollectionBulkDelete(ModelBulkDeleteMutation):
      class Arguments:
          ids = graphene.List(
              graphene.ID, required=True, description="List of collection IDs to delete."
          )
  
      class Meta:
          description = "Deletes collections."
          model = models.Collection
          permissions = ("product.manage_products",)
          error_type_class = ProductError
          error_type_field = "product_errors"
  
  
  class CollectionBulkPublish(BaseBulkMutation):
      class Arguments:
          ids = graphene.List(
              graphene.ID,
              required=True,
              description="List of collections IDs to (un)publish.",
          )
          is_published = graphene.Boolean(
              required=True,
              description="Determine if collections will be published or not.",
          )
  
      class Meta:
          description = "Publish collections."
          model = models.Collection
          permissions = ("product.manage_products",)
          error_type_class = ProductError
          error_type_field = "product_errors"
  
      @classmethod
      def bulk_action(cls, queryset, is_published):
          queryset.update(is_published=is_published)
  
  
  class ProductBulkDelete(ModelBulkDeleteMutation):
      class Arguments:
          ids = graphene.List(
              graphene.ID, required=True, description="List of product IDs to delete."
          )
  
      class Meta:
          description = "Deletes products."
          model = models.Product
          permissions = ("product.manage_products",)
          error_type_class = ProductError
          error_type_field = "product_errors"
  
  
  class ProductVariantBulkCreateInput(ProductVariantInput):
      attributes = graphene.List(
          AttributeValueInput,
          required=True,
          description="List of attributes specific to this variant.",
      )
      sku = graphene.String(required=True, description="Stock keeping unit.")
  
  
  class ProductVariantBulkCreate(BaseMutation):
      count = graphene.Int(
          required=True,
          default_value=0,
          description="Returns how many objects were created.",
      )
      product_variants = graphene.List(
          graphene.NonNull(ProductVariant),
          required=True,
          default_value=[],
          description="List of the created variants.",
      )
  
      class Arguments:
          variants = graphene.List(
              ProductVariantBulkCreateInput,
              required=True,
              description="Input list of product variants to create.",
          )
          product_id = graphene.ID(
              description="ID of the product to create the variants for.",
              name="product",
              required=True,
          )
  
      class Meta:
          description = "Creates product variants for a given product."
          permissions = ("product.manage_products",)
          error_type_class = BulkProductError
          error_type_field = "bulk_product_errors"
  
      @classmethod
      def clean_input(cls, info, instance: models.ProductVariant, data: dict):
          cleaned_input = ModelMutation.clean_input(
              info, instance, data, input_cls=ProductVariantBulkCreateInput
          )
  
          cost_price_amount = cleaned_input.pop("cost_price", None)
          if cost_price_amount is not None:
              cleaned_input["cost_price_amount"] = cost_price_amount
  
          price_override_amount = cleaned_input.pop("price_override", None)
          if price_override_amount is not None:
              cleaned_input["price_override_amount"] = price_override_amount
  
          attributes = cleaned_input.get("attributes")
          if attributes:
              try:
                  cleaned_input["attributes"] = ProductVariantCreate.clean_attributes(
                      attributes, data["product_type"]
                  )
              except ValidationError as exc:
                  raise ValidationError({"attributes": exc})
  
          return cleaned_input
  
      @classmethod
      def add_indexes_to_errors(cls, index, error, error_dict):
          """Append errors with index in params to mutation error dict."""
          for key, value in error.error_dict.items():
              for e in value:
                  if e.params:
                      e.params["index"] = index
                  else:
                      e.params = {"index": index}
              error_dict[key].extend(value)
  
      @classmethod
      def save(cls, info, instance, cleaned_input):
          instance.save()
  
          attributes = cleaned_input.get("attributes")
          if attributes:
              AttributeAssignmentMixin.save(instance, attributes)
              instance.name = generate_name_for_variant(instance)
              instance.save(update_fields=["name"])
  
      @classmethod
      def create_variants(cls, info, cleaned_inputs, product, errors):
          instances = []
          for index, cleaned_input in enumerate(cleaned_inputs):
              if not cleaned_input:
                  continue
              try:
                  instance = models.ProductVariant()
                  cleaned_input["product"] = product
                  instance = cls.construct_instance(instance, cleaned_input)
                  cls.clean_instance(instance)
                  instances.append(instance)
              except ValidationError as exc:
                  cls.add_indexes_to_errors(index, exc, errors)
          return instances
  
      @classmethod
      def validate_duplicated_sku(cls, sku, index, sku_list, errors):
          if sku in sku_list:
              errors["sku"].append(
                  ValidationError(
                      "Duplicated SKU.", ProductErrorCode.UNIQUE, params={"index": index}
                  )
              )
          sku_list.append(sku)
  
      @classmethod
      def clean_variants(cls, info, variants, product, errors):
          cleaned_inputs = []
          sku_list = []
          used_attribute_values = get_used_variants_attribute_values(product)
          for index, variant_data in enumerate(variants):
              try:
                  ProductVariantCreate.validate_duplicated_attribute_values(
                      variant_data.attributes, used_attribute_values
                  )
              except ValidationError as exc:
                  errors["attributes"].append(
                      ValidationError(exc.message, exc.code, params={"index": index})
                  )
  
              cleaned_input = None
              try:
                  variant_data["product_type"] = product.product_type
                  cleaned_input = cls.clean_input(info, None, variant_data)
              except ValidationError as exc:
                  cls.add_indexes_to_errors(index, exc, errors)
              cleaned_inputs.append(cleaned_input if cleaned_input else None)
  
              if not variant_data.sku:
                  continue
              cls.validate_duplicated_sku(variant_data.sku, index, sku_list, errors)
          return cleaned_inputs
  
      @classmethod
      @transaction.atomic
      def save_variants(cls, info, instances, cleaned_inputs):
          assert len(instances) == len(
              cleaned_inputs
          ), "There should be the same number of instances and cleaned inputs."
          for instance, cleaned_input in zip(instances, cleaned_inputs):
              cls.save(info, instance, cleaned_input)
  
      @classmethod
      def perform_mutation(cls, root, info, **data):
          product = cls.get_node_or_error(info, data["product_id"], models.Product)
          errors = defaultdict(list)
  
          cleaned_inputs = cls.clean_variants(info, data["variants"], product, errors)
          instances = cls.create_variants(info, cleaned_inputs, product, errors)
          if errors:
              raise ValidationError(errors)
          cls.save_variants(info, instances, cleaned_inputs)
  
          # Recalculate the "minimal variant price" for the parent product
          update_product_minimal_variant_price_task.delay(product.pk)
  
          return ProductVariantBulkCreate(
              count=len(instances), product_variants=instances
          )
  
      @classmethod
      def handle_typed_errors(cls, errors: list, **extra):
          typed_errors = [
              cls._meta.error_type_class(
                  field=e.field,
                  message=e.message,
                  code=code,
                  index=params.get("index") if params else None,
              )
              for e, code, params in errors
          ]
          extra.update({cls._meta.error_type_field: typed_errors})
          return cls(errors=[e[0] for e in errors], **extra)
  
  
  class ProductVariantBulkDelete(ModelBulkDeleteMutation):
      class Arguments:
          ids = graphene.List(
              graphene.ID,
              required=True,
              description="List of product variant IDs to delete.",
          )
  
      class Meta:
          description = "Deletes product variants."
          model = models.ProductVariant
          permissions = ("product.manage_products",)
          error_type_class = ProductError
          error_type_field = "product_errors"
  
  
  class ProductTypeBulkDelete(ModelBulkDeleteMutation):
      class Arguments:
          ids = graphene.List(
              graphene.ID,
              required=True,
              description="List of product type IDs to delete.",
          )
  
      class Meta:
          description = "Deletes product types."
          model = models.ProductType
          permissions = ("product.manage_products",)
          error_type_class = ProductError
          error_type_field = "product_errors"
  
  
  class ProductImageBulkDelete(ModelBulkDeleteMutation):
      class Arguments:
          ids = graphene.List(
              graphene.ID,
              required=True,
              description="List of product image IDs to delete.",
          )
  
      class Meta:
          description = "Deletes product images."
          model = models.ProductImage
          permissions = ("product.manage_products",)
          error_type_class = ProductError
          error_type_field = "product_errors"
  
  
  class ProductBulkPublish(BaseBulkMutation):
      class Arguments:
          ids = graphene.List(
              graphene.ID, required=True, description="List of products IDs to publish."
          )
          is_published = graphene.Boolean(
              required=True, description="Determine if products will be published or not."
          )
  
      class Meta:
          description = "Publish products."
          model = models.Product
          permissions = ("product.manage_products",)
          error_type_class = ProductError
          error_type_field = "product_errors"
  
      @classmethod
      def bulk_action(cls, queryset, is_published):
          queryset.update(is_published=is_published)
  