diff --git a/config/schema/commerce.schema.yml b/config/schema/commerce.schema.yml index bd7bed11b0..f6904610e3 100644 --- a/config/schema/commerce.schema.yml +++ b/config/schema/commerce.schema.yml @@ -76,6 +76,64 @@ field.widget.settings.commerce_entity_select: type: string label: 'Autocomplete placeholder' +field.widget.settings.commerce_auto_sku: + type: mapping + label: 'Commerce auto SKU format settings' + mapping: + custom_label: + type: label + label: 'Custom label' + uniqid_enabled: + type: boolean + label: 'Enable unique auto SKU values generation' + more_entropy: + type: boolean + label: 'More unique' + hide: + type: boolean + label: 'Hide SKU' + prefix: + type: label + label: 'SKU prefix' + suffix: + type: label + label: 'SKU suffix' + size: + type: integer + label: 'Size of textfield' + placeholder: + type: label + label: 'Placeholder' + +field.widget.settings.commerce_auto_sku: + type: mapping + label: 'Commerce auto SKU format settings' + mapping: + custom_label: + type: label + label: 'Custom label' + uniqid_enabled: + type: boolean + label: 'Enable unique auto SKU values generation' + more_entropy: + type: boolean + label: 'More unique' + hide: + type: boolean + label: 'Hide SKU' + prefix: + type: label + label: 'SKU prefix' + suffix: + type: label + label: 'SKU suffix' + size: + type: integer + label: 'Size of textfield' + placeholder: + type: label + label: 'Placeholder' + views.argument_validator.commerce_current_user: type: mapping label: 'Current user' diff --git a/modules/product/commerce_product.module b/modules/product/commerce_product.module index 313362a0b4..db4a98121b 100644 --- a/modules/product/commerce_product.module +++ b/modules/product/commerce_product.module @@ -259,12 +259,153 @@ function commerce_product_field_widget_form_alter(&$element, FormStateInterface $entity_type = $field_definition->getTargetEntityTypeId(); $widget_name = $context['widget']->getPluginId(); $required = $field_definition->isRequired(); - if ($field_name == 'path' && $entity_type == 'commerce_product' && $widget_name == 'path') { - $element['alias']['#description'] = t('The alternative URL for this product. Use a relative path. For example, "/my-product".'); - } - elseif ($field_name == 'title' && $entity_type == 'commerce_product_variation' && !$required) { - // The title field is optional only when its value is automatically - // generated, in which case the widget needs to be hidden. - $element['#access'] = FALSE; + switch (TRUE) { + // The filtering only current owner stores is now moved to: https://github.com/drugan/commerce_multistore +// case ($field_name == 'stores'): +// $user = \Drupal::currentUser(); +// // If current user has no admin permission then display only those stores +// // which are owned by the user. +// if (!$user->hasPermission('administer commerce_store')) { +// $stores = \Drupal::service('entity.manager')->getStorage('commerce_store')->loadMultiple(); +// $id = $user->id(); +// $i = 0; +// foreach ($stores as $store) { +// if ($store->getOwnerId() != $id) { +// $element['alter_data_' . $i] = [ +// '#parents' => ['target_id', 'value'], +// $store->id() => [], +// ]; +// $i++; +// } +// } +// if ($i) { +// $creator = \Drupal::service('commerce_product.variation_bulk_creator'); +// $element['#after_build'][] = [get_class($creator), 'afterBuildPreRenderArrayAlter']; +// } +// } +// break; + case ($field_name == 'variations' && $entity_type == 'commerce_product'): + /** @var \Drupal\commerce_product\ProductVariationBulkCreator $creator */ + $creator = \Drupal::service('commerce_product.variation_bulk_creator'); + /** @var \Drupal\commerce_product\Entity\Product $product */ + $product = $form_state->getFormObject()->getEntity(); + /** @var \Drupal\commerce_product\Entity\ProductVariation $variation */ + $variation = $creator->getProductVariation($product); + $sku_settings = $creator::getSkuwidget($variation)->getSettings(); + $form_state->setValue('sku_settings', $sku_settings); + extract($sku_settings); + $widget_settings = $context['widget']->getSettings(); + $one = isset($widget_settings['label_singular']) ? $widget_settings['label_singular'] : FALSE; + $many = $one ? $widget_settings['label_plural'] : FALSE; + // Disable autocompletion and autocreation if the widget is not inline + // entity form or if all commerce SKU settings are empty. + if (!$one || (empty($uniqid_enabled) && empty($prefix) && empty($suffix))) { + break; + } + $ief = isset($element['form']['inline_entity_form']); + $add = isset($element['actions']['ief_add']); + $add_existing = isset($element['actions']['ief_add_existing']); + $autocomplete = isset($element['form']['entity_id']); + $id = isset($element['#ief_id']) ? $element['#ief_id'] : FALSE; + if ($new = !isset($element['entities']['0'])) { + $all = $creator->getNotUsedAttributesCombination([$variation]); + $count = $all ? $all['count'] : 0; + if ($count > 1 && ($add || $add_existing || $autocomplete || $ief)) { + $i = 0; + // The disabled button is shown to reveal the bulk creator service + // availability and the number of variations to create. + $parents = ['form', 'inline_entity_form', 'actions', 'ief_add_all']; + $element['alter_data_' . $i] = [ + '#parents' => $add_existing ? ['actions'] : ($autocomplete ? ['form', 'actions'] : $parents), + '#type' => 'submit', + '#value' => t('Create @count @variations', ['@count'=> $count, '@variations' => $count > 1 ? $many : $one]), + '#weight' => 10, + '#disabled' => TRUE, + '#attributes'=> [ + 'title' => t('To enable this functionality you need to create at least one @variation', ['@variation'=> $one]), + ], + ]; + if ($ief) { + $i++; + foreach ($all['not_used_combination'] as $field_name => $value) { + // Do respect the default value defined on the attribute field. + if (empty($variation->getFieldDefinition($field_name)->getDefaultValueLiteral())) { + $element['alter_data_' . $i] = [ + '#parents' => ['form', 'inline_entity_form', $field_name, 'widget'], + '#value' => $value == '_none' ? [] : ['target_id' => $value], + ]; + $i++; + } + } + } + $element['#after_build'][] = [get_class($creator), 'afterBuildPreRenderArrayAlter']; + } + } + elseif ($id && $all = $creator->getIefFormNotUsedAttributesCombination($form_state, $id)) { + if ($all['duplicated']) { + $placeholders = [ + '@variations' => $all['duplicated'] > 1 ? $many : $one, + '@count' => $all['duplicated'], + '@labels' => $all['duplications_list'], + ]; + $warning = t('You have @count @variations duplicated: @labels', $placeholders); + drupal_set_message($warning, 'warning'); + } + if (!empty($all['not_used_combination'])) { + $form_state->setValue('next_attribute_value_ids', $all['not_used_combination']); + /** @var \Drupal\commerce_product\Entity\ProductVariation $all['last_variation'] */ + !$new && $form_state->setValue('last_added_price', $all['last_variation']->getPrice()); + if (!$new && ($add || $add_existing)) { + $count = $all['count'] - $all['used']; + $not_all = isset($all['not_all']) ? $all['not_all'] : 0; + $out_of = $not_all && $all['count'] > $not_all ? t('(out of @count)', ['@count'=> $count]) : ''; + $value = t('Create @not_used @out_of @variations (done @used)', ['@used' => $all['used'], '@not_used' => $all['not_used'], '@out_of'=> $out_of, '@variations' => $count > 1 ? $many : $one]); + $warning = t('Be reasonable! An attempt to autocreate a huge amount of data in one go may not work and even freeze your system.'); + $description = t('Automatically create @variations based on price and attributes combination of the last added @variation. Other specific values such as @variation image or SKU may be edited inline later. Helpful for not missing any @variation.', + ['@variation'=> $one, '@variations'=> $many]); + $element['actions']['ief_add_all'] = [ + '#submit' => [[$creator, 'createAllIefFormVariations']], + '#value' => $value, + '#weight' => 10, + '#attributes'=> [ + 'title' => $not_all && $all['used'] > $not_all ? $warning : $description, + ], + ] + $element['actions']['ief_add' . ($add ? '' : '_existing')]; + } + } + } + break; + case ($field_name == 'path' && $entity_type == 'commerce_product' && $widget_name == 'path'): + $element['alias']['#description'] = t('The alternative URL for this product. Use a relative path. For example, "/my-product".'); + break; + case ($field_name == 'title' && $entity_type == 'commerce_product_variation' && !$required): + // The title field is optional only when its value is automatically + // generated, in which case the widget needs to be hidden. + $element['#access'] = FALSE; + break; + case ($field_name == 'price' && empty($element['#default_value']) && ($price = $form_state->getValue('last_added_price'))): + $element['#default_value'] = $price->toArray(); + break; + case (($ids = $form_state->getValue('next_attribute_value_ids')) && isset($ids[$field_name]) && empty($element['#default_value'])): + $element['#default_value'] = $ids[$field_name]; + break; + case ($field_name == 'sku'): + if (!$sku_settings = $form_state->getValue('sku_settings')) { + $creator = \Drupal::service('commerce_product.variation_bulk_creator'); + $product = $form_state->getFormObject()->getEntity(); + $variation = $creator->getProductVariation($product); + $sku_settings = $creator::getSkuwidget($variation)->getSettings(); + } + extract($sku_settings); + if (!empty($uniqid_enabled) && $hide) { + $element['value']['#type'] = 'value'; + $element['value']['#value'] = $element['value']['#default_value']; + } + else { + global $base_url; + $setup_link = t('Set up default SKU.', [':href' => $base_url . '/admin/commerce/config/product-variation-types/' . $context['form']['#bundle'] . '/edit/form-display']); + $element['value']['#description'] = implode(' ', [$element['value']['#description'], $setup_link]); + } + break; } } diff --git a/modules/product/commerce_product.services.yml b/modules/product/commerce_product.services.yml index cca824dbe1..af487f7aae 100644 --- a/modules/product/commerce_product.services.yml +++ b/modules/product/commerce_product.services.yml @@ -10,3 +10,7 @@ services: commerce_product.variation_field_renderer: class: Drupal\commerce_product\ProductVariationFieldRenderer arguments: ['@entity_type.manager', '@entity_field.manager'] + + commerce_product.variation_bulk_creator: + class: Drupal\commerce_product\ProductVariationBulkCreator + arguments: ['@entity_type.manager'] diff --git a/modules/product/config/install/core.entity_form_display.commerce_product_variation.default.default.yml b/modules/product/config/install/core.entity_form_display.commerce_product_variation.default.default.yml index c699947d4c..1d4fc86feb 100644 --- a/modules/product/config/install/core.entity_form_display.commerce_product_variation.default.default.yml +++ b/modules/product/config/install/core.entity_form_display.commerce_product_variation.default.default.yml @@ -16,9 +16,15 @@ content: settings: { } third_party_settings: { } sku: - type: string_textfield + type: commerce_auto_sku weight: -4 settings: + custom_label: '' + uniqid_enabled: true + more_entropy: false + hide: false + prefix: 'sku-' + suffix: '' size: 60 placeholder: '' third_party_settings: { } diff --git a/modules/product/src/Entity/ProductVariation.php b/modules/product/src/Entity/ProductVariation.php index 052607be27..6b968314a2 100644 --- a/modules/product/src/Entity/ProductVariation.php +++ b/modules/product/src/Entity/ProductVariation.php @@ -392,11 +392,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['sku'] = BaseFieldDefinition::create('string') ->setLabel(t('SKU')) ->setDescription(t('The unique, machine-readable identifier for a variation.')) + ->setDefaultValueCallback('Drupal\commerce_product\ProductVariationBulkCreator::getAutoSku') ->setRequired(TRUE) ->addConstraint('ProductVariationSku') ->setSetting('display_description', TRUE) ->setDisplayOptions('form', [ - 'type' => 'string_textfield', + 'type' => 'commerce_auto_sku', 'weight' => -4, ]) ->setDisplayConfigurable('form', TRUE) diff --git a/modules/product/src/ProductVariationBulkCreator.php b/modules/product/src/ProductVariationBulkCreator.php new file mode 100644 index 0000000000..9ba4fa8d77 --- /dev/null +++ b/modules/product/src/ProductVariationBulkCreator.php @@ -0,0 +1,409 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function getSkuwidget(ProductVariation $variation) { + $form_display = entity_get_form_display($variation->getEntityTypeId(), $variation->bundle(), 'default'); + + return $form_display->getRenderer('sku'); + } + + /** + * {@inheritdoc} + */ + public static function getSkuSettings(ProductVariation $variation) { + /** @var Drupal\commerce_product\Plugin\Field\FieldWidget\ProductVariationSkuWidget $widget */ + /** @var Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget $widget */ + $widget = static::getSkuwidget($variation); + // If no one widget is enabled, then we need to asign uniqid() SKUs at the + // background to avoid having variations without SKU at all. + $default_sku_settings = [ + 'uniqid_enabled' => TRUE, + 'more_entropy' => FALSE, + 'prefix' => 'default_sku-', + 'suffix' => '', + ]; + + return $widget ? $widget->getSettings() : $default_sku_settings; + } + + /** + * {@inheritdoc} + */ + public static function getAutoSku(ProductVariation $variation) { + extract(static::getSkuSettings($variation)); + + // Do return empty string in case of StringTextfieldWidget. + return isset($uniqid_enabled) ? ($uniqid_enabled ? \uniqid($prefix, $more_entropy) . $suffix : "{$prefix}{$suffix}") : ''; + } + + /** + * {@inheritdoc} + */ + public static function afterBuildPreRenderArrayAlter(array $element) { + $i = 0; + while (isset($element['alter_data_' . $i]) && $data = $element['alter_data_' . $i]) { + $parents = []; + if (isset($data['#parents'])) { + $parents = $data['#parents']; + unset($data['#parents']); + } + unset($element['alter_data_' . $i]); + $key_exists = NULL; + $old_data = NestedArray::getValue($element, $parents, $key_exists); + if (is_array($old_data)) { + $data = array_replace($old_data, $data); + } + elseif ($key_exists && !in_array($old_data, $data)) { + $data[] = $old_data; + } + NestedArray::setValue($element, $parents, $data); + $i++; + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public function getProductVariation(Product $product) { + $variations = $product->getVariations(); + $variation = end($variations); + $timestamp = time(); + if (!$variation instanceof ProductVariation) { + $variation = $this->entityTypeManager->getStorage('commerce_product_variation')->create([ + 'type' => $product->getFieldDefinition('variations')->getSettings()['handler_settings']['target_bundles'][0], + 'created' => $timestamp, + 'changed' => $timestamp, + ]); + } + + return $variation; + } + + /** + * {@inheritdoc} + */ + public function createProductVariation(Product $product, array $variation_custom_values = []) { + $variation = $this->getProductVariation($product); + $field = $this->getAttributeFieldOptionIds($variation); + if ($all = $this->getAttributesCombinations([$variation])) { + foreach (reset($all['not_used_combinations']) as $field_name => $id) { + $variation->get($field_name)->setValue(['target_id' => $id == '_none' ? NULL : $id]); + } + } + $sku = static::getAutoSku($variation); + $variation->setSku(empty($sku) ? \uniqid() : $sku); + + foreach ($variation_custom_values as $name => $value) { + $variation->set($name, $value); + } + if (!$variation->getPrice() instanceof Price) { + $currency_storage = $this->entityTypeManager->getStorage('commerce_currency'); + $currencies = array_keys($currency_storage->loadMultiple()); + $currency = empty($currencies) ? 'USD' : $currencies[0]; + // Decimals are omitted intentionally as $currency format is unknown here. + // The prices still will have valid format after saving. + $variation->setPrice(new Price('1', $currency)); + } + $variation->updateOriginalValues(); + + return $variation; + } + + /** + * {@inheritdoc} + */ + public function createAllProductVariations(Product $product, array $variation_custom_values = []) { + $variations = $product->getVariations(); + $timestamp = time(); + if (empty($variations) || !empty($variation_custom_values)) { + $variations[] = $this->createProductVariation($product, $variation_custom_values); + $timestamp--; + } + + if (!$all = $this->getAttributesCombinations($variations)) { + return; + } + + // Improve perfomance by getting sku settings just once instead of + // calling static::getAutoSku() in the loop. + extract(static::getSkuSettings($all['last_variation'])); + $prefix = isset($prefix) ? $prefix : ''; + $suffix = isset($suffix) ? $suffix : ''; + $more_entropy = isset($more_entropy) ? $more_entropy : FALSE; + foreach ($all['not_used_combinations'] as $combination) { + $variation = $all['last_variation']->createDuplicate() + ->setSku(\uniqid($prefix, $more_entropy) . $suffix) + ->setChangedTime($timestamp) + ->setCreatedTime($timestamp); + foreach ($combination as $field_name => $id) { + $variation->get($field_name)->setValue(['target_id' => $id == '_none' ? NULL : $id]); + } + $variation->updateOriginalValues(); + $variations[] = $variation; + // To avoid the same CreatedTime on multiple variations decrease the + // $timestamp by one second instead of calling time() in the loop. + $timestamp--; + } + + return $variations; + } + + /** + * {@inheritdoc} + */ + public function createAllIefFormVariations(array $form, FormStateInterface $form_state) { + // Rid of entity type manager here as that prevents to use instance of + // ProductVariationBulkCreator as an AJAX callback therefore forcing to use + // just the class name instead of object and define all functions as static. + $this->entityTypeManager = NULL; + $ief_id = $form['variations']['widget']['#ief_id']; + $ief_entities = $form_state->get(['inline_entity_form', $ief_id, 'entities']) ?: []; + if (!$all = $this->getAttributesCombinations(array_column($ief_entities, 'entity'))) { + return; + } + // The attributes (ids and options) may be quite heavy, so unset them. + unset($all['attributes']); + $timestamp = time(); + $ief_entity = end($ief_entities); + extract(static::getSkuSettings($all['last_variation'])); + $prefix = isset($prefix) ? $prefix : ''; + $suffix = isset($suffix) ? $suffix : ''; + $more_entropy = isset($more_entropy) ? $more_entropy : FALSE; + foreach ($all['not_used_combinations'] as $combination) { + $variation = $all['last_variation']->createDuplicate() + ->setSku(\uniqid($prefix, $more_entropy) . $suffix) + ->setChangedTime($timestamp) + ->setCreatedTime($timestamp); + foreach ($combination as $field_name => $id) { + $variation->get($field_name)->setValue(['target_id' => $id == '_none' ? NULL : $id]); + } + $variation->updateOriginalValues(); + $ief_entity['entity'] = $variation; + $ief_entity['weight'] += 1; + $ief_entity['needs_save'] = TRUE; + array_push($ief_entities, $ief_entity); + $timestamp--; + } + // Before continuing unset $all['*combinations'] which might be a huge data. + unset($all); + $form_state->set(['inline_entity_form', $ief_id, 'entities'], $ief_entities); + $form_state->setRebuild(); + } + + /** + * {@inheritdoc} + */ + public function getIefFormNotUsedAttributesCombination(FormStateInterface $form_state, $ief_id = '') { + $this->entityTypeManager = NULL; + $ief_entities = $form_state->get(['inline_entity_form', $ief_id, 'entities']) ?: []; + + return $this->getNotUsedAttributesCombination(array_column($ief_entities, 'entity')); + } + + /** + * {@inheritdoc} + */ + public function getNotUsedAttributesCombination(array $variations) { + if (!$all = $this->getDuplicationsHtmlList($variations)) { + return; + } + $all['not_used_combination'] = reset($all['not_used_combinations']); + // Rid of unecessary data which might be quite heavy. + unset($all['used_combinations'], $all['not_used_combinations'], $all['attributes']); + + return $all; + } + + /** + * {@inheritdoc} + */ + public function getUsedAttributesCombinations(array $variations) { + $all = []; + $all['duplicated'] = $all['used_combinations'] = []; + $all['last_variation'] = end($variations); + $all['attributes'] = $this->getAttributeFieldOptionIds(end($variations)); + $nones = array_fill_keys(array_keys($all['attributes']['ids']), '_none'); + foreach ($variations as $index => $variation) { + // ProductVariation->getAttributeValueIds() does not return empty optional + // fields. Merge 'field_name' => '_none' as a choice in the combination. + // @todo Render '_none' option on an Add to Cart form. + // @see ProductVariationAttributesWidget->formElement() + // @see CommerceProductRenderedAttribute::processRadios() + $combination = array_merge($nones, $variation->getAttributeValueIds()); + if (in_array($combination, $all['used_combinations'])) { + $all['duplicated'][$index] = $combination; + } + else { + $all['used_combinations'][$index] = $combination; + } + } + $all['used'] = count($all['used_combinations']); + $all['count'] = $all['attributes']['count']; + + return $all; + } + + /** + * {@inheritdoc} + */ + public function getDuplicationsHtmlList(array $variations) { + if (!$all = $this->getAttributesCombinations($variations)) { + return; + } + if (!empty($all['duplicated'])) { + $all['duplications_list'] = ''; + $all['duplications_list'] = Markup::create($all['duplications_list']); + } + $all['duplicated'] = count($all['duplicated']); + + return $all; + } + + /** + * {@inheritdoc} + */ + public function getAttributesCombinations(array $variations, array $return = ['not_all' => 500]) { + $all = $this->getUsedAttributesCombinations($variations); + // Restrict by default the number of returned not used combinations if their + // number exceeds some resonable number (500). To get all possible + // combinations call this method with an empty array as the second argument. + if (isset($return['not_all']) && $all['count'] > $return['not_all']) { + $all += $return; + $all['used_combinations']['not_all'] = $return['not_all']; + } + $all['not_used_combinations'] = $this->getArrayValueCombinations($all['attributes']['ids'], $all['used_combinations']); + unset($all['used_combinations']['not_all']); + $all['not_used'] = count($all['not_used_combinations']); + + return $all; + } + + /** + * {@inheritdoc} + */ + public function getArrayValueCombinations(array $data = [], array $exclude = [], &$all = [], $group = [], $value = NULL, $i = 0, $k = NULL, $c = NULL, $f = NULL) { + $keys = $k ?: array_keys($data); + $count = $c ?: count($data); + if ($include = isset($value) === TRUE) { + $group[$f] = $value; + } + if ($i >= $count && $include) { + foreach ($exclude as $index => $combination) { + if ($group == $combination) { + unset($exclude[$index]); + $include = FALSE; + break; + } + } + if ($include) { + $all[] = $group; + } + } + elseif (isset($keys[$i])) { + if (isset($exclude['not_all']) && !empty($all) && count($all) > $exclude['not_all']) { + return $all; + } + $field_name = $keys[$i]; + foreach ($data[$field_name] as $key => $val) { + $this->getArrayValueCombinations($data, $exclude, $all, $group, $val, $i + 1, $keys, $count, $field_name); + } + } + + return $all; + } + + /** + * {@inheritdoc} + */ + public function getAttributeFieldOptionIds(ProductVariation $variation) { + $count = 1; + $field_options = $fields = $ids = $options = []; + foreach ($this->getAttributeFieldNames($variation) as $field_name) { + $definition = $variation->get($field_name)->getFieldDefinition(); + $fields[$field_name] = $definition->getFieldStorageDefinition() + ->getOptionsProvider('target_id', $variation) + ->getSettableOptions(\Drupal::currentUser()); + $ids[$field_name] = $options[$field_name] = []; + foreach ($fields[$field_name] as $key => $value) { + if (is_array($value) && $keys = array_keys($value)) { + $ids[$field_name] = array_unique(array_merge($ids[$field_name], $keys)); + $options[$field_name] += $value; + } + elseif ($keys = array_keys($fields[$field_name])) { + $ids[$field_name] = array_unique(array_merge($ids[$field_name], $keys)); + $options[$field_name] += $fields[$field_name]; + } + // Optional fields need '_none' id as a possible choice. + !$definition->isRequired() && !in_array('_none', $ids[$field_name]) && array_unshift($ids[$field_name], '_none'); + array_walk($ids[$field_name], function (&$id) { + $id = (string) $id; + }); + } + $count *= count($ids[$field_name]); + } + $field_options['ids'] = $ids; + $field_options['options'] = $options; + $field_options['count'] = $count; + + return $field_options; + } + + /** + * {@inheritdoc} + */ + public function getAttributeFieldNames(ProductVariation $variation) { + $attribute_field_manager = \Drupal::service('commerce_product.attribute_field_manager'); + $field_map = $attribute_field_manager->getFieldMap($variation->bundle()); + + return array_unique(array_column($field_map, 'field_name')); + } + +} diff --git a/modules/product/src/ProductVariationBulkCreatorInterface.php b/modules/product/src/ProductVariationBulkCreatorInterface.php new file mode 100644 index 0000000000..63b895651c --- /dev/null +++ b/modules/product/src/ProductVariationBulkCreatorInterface.php @@ -0,0 +1,317 @@ + ['form', 'deep', 'nested', 'array_element'], + * '#default_value' => $my_value, + * // ... + * ]; + * $i++; + * $element['alter_data_' . $i] = [ + * '#parents' => ['form', 'another', 'nested', 'array_element'], + * '#disabled' => TRUE, + * // ... + * ]; + * // @var \Drupal\commerce_product\ProductVariationBulkCreator $creator + * $element['#after_build'][] = [$creator, 'afterBuildPreRenderArrayAlter']; + * @endcode + * It is primarily used for form structures and renderable arrays. Any number + * of data arrays with different paths (#parents) may be attached to an + * element. If #parents is omitted the altering will apply on the root of the + * element. The $creator may be passed to a callbacks array as an object or + * a fully qualified class name. After the target array elements being altered + * the 'alter_data_NNN' containers are unset. + * + * @param array $element + * The render array element normally passed by the system call. + * + * @return array + * The altered render array element. + * + * @see commerce_product_field_widget_form_alter() + */ + public static function afterBuildPreRenderArrayAlter(array $element); + + /** + * Gets a variation for commerce_product. + * + * @param \Drupal\commerce_product\Entity\Product $product + * A commerce product, whether new or having some variations saved on it. + * + * @return \Drupal\commerce_product\Entity\ProductVariation + * If exists, the last variation on a commerce_product, otherwise new one. + * + * @see \Drupal\commerce_product\Entity\ProductVariation->create() + * @see self->createProductVariation() + */ + public function getProductVariation(Product $product); + + /** + * Creates a variation for commerce_product. + * + * @param \Drupal\commerce_product\Entity\Product $product + * A commerce product, whether new or having some variations saved on it. + * @param array $variation_custom_values + * (optional) An associative array of a variation property values which + * will be used to auto create sample variation. + * + * @return \Drupal\commerce_product\Entity\ProductVariation + * A commerce_product variation. + * + * @see \Drupal\commerce_product\Entity\ProductVariation->create() + * @see self->createAllProductVariations() + */ + public function createProductVariation(Product $product, array $variation_custom_values = []); + + /** + * Creates all possible variations for commerce_product. + * + * @param \Drupal\commerce_product\Entity\Product $product + * A commerce product, whether new or having some variations saved on it. + * @param array $variation_custom_values + * (optional) An associative array of a variation property values which + * will be used to auto create all variations. + * + * @return array|null + * An array of all commerce product variations that were missed before. + * + * @see \Drupal\commerce_product\Entity\Product->getVariations() + * @see self->getAllAttributesCombinations() + */ + public function createAllProductVariations(Product $product, array $variation_custom_values = []); + + /** + * An AJAX callback to create all possible variations on the commerce product + * add or edit form. + * + * @param array $form + * An array form for commerce_product with ief widget. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state of the commerce_product form with at least one variation + * created. + * + * @see self->getIefFormAllAttributesCombinations() + */ + public function createAllIefFormVariations(array $form, FormStateInterface $form_state); + + /** + * Gets first not used combination on a product IEF form. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state of the commerce_product form with at least one variation + * created. + * @param string $ief_id + * A product form IEF widget id. + * + * @return array|null + * a number of possible and duplicated and used combinations, last + * variation, number of duplicated combinations and an HTML list of + * duplicated variations labels if they are found: + * - "last_variation": The variation on the last inline entity form array. + * - "count": The number of all combinations. + * - "duplicated": The number of duplicated combinations. + * - "used": The number of used combinations. + * - "duplications_list": HTML list of duplicated combinations if present. + * - "not_used_combination": The first not used attributes combination. + */ + public function getIefFormNotUsedAttributesCombination(FormStateInterface $form_state, $ief_id = ''); + + /** + * Gets first not used combination on a product. + * + * @param array $variations + * The commerce product variations. + * + * @return array|null + * a number of possible and duplicated and used combinations, last + * variation, number of duplicated combinations and an HTML list of + * duplicated variations labels if they are found: + * - "last_variation": The variation on the last inline entity form array. + * - "count": The quantity of the combinations. + * - "duplicated": The number of duplicated combinations. + * - "used": The number of used combinations. + * - "duplications_list": HTML list of duplicated combinations if present. + * - "not_used_combination": The first not used attributes combination. + */ + public function getNotUsedAttributesCombination(array $variations); + + /** + * Gets used combinations on a product. + * + * @param array $variations + * The commerce product variations. + * + * @return array|null + * last variation, variation attributes ids and options and already used + * used attributes combinations, if they are found: + * - "last_variation": The variation on the last inline entity form array. + * - "attributes": An array with attributes ids and options: + * - "ids": The array of field_name => id pairs. + * - "options": The array of id => field_label pairs. + * - "used_combinations": The already used attributes combinations. + */ + public function getUsedAttributesCombinations(array $variations); + + /** + * Gets duplicated variations HTML list. + * + * @param array $variations + * The commerce product variations. + * + * @return array|null + * An array of used combinations, not used combinations and their number, + * last variation, variation attributes ids and options, and an HTML list of + * duplicated variations labels if they are found: + * - "last_variation": The variation on the last inline entity form array. + * - "used_combinations": The already used combinations. + * - "duplicated": The number of duplicated combinations. + * - "used": The number of used combinations. + * - "duplications_list": HTML list of duplicated combinations if present. + * - "attributes": An array with attributes ids and options: + * - "ids": The array of field_name => id pairs. + * - "options": The array of id => field_label pairs. + * - "not_all": The maximum number of combinations to return. + */ + public function getDuplicationsHtmlList(array $variations); + + /** + * Gets all ids combinations of the commerce_product's attribute fields. + * + * @param array $variations + * The commerce product variations. + * @param array $return + * (optional) Whether to return all attributes combinations or just ~ 500. + * + * @return array|null + * An array of used combinations, not used combinations and their number, + * last variation, variation attributes ids and options: + * - "last_variation": The variation on the last inline entity form array. + * - "used_combinations": The already used combinations. + * - "not_used_combinations": Yet not used combinations. + * - "count": The number of all combinations. + * - "attributes": An array with attributes ids and options: + * - "ids": The array of field_name => id pairs. + * - "options": The array of id => field_label pairs. + * - "duplicated": The number of duplicated combinations. + * - "used": The number of used combinations. + * - "not_all": The maximum number of combinations to return. + */ + public function getAttributesCombinations(array $variations, array $return = ['not_all' => 500]); + + /** + * Gets combinations of an Array values. + * + * See the function + * @link https://gist.github.com/fabiocicerchia/4556892 source origin @endlink + * . + * + * @param array $data + * An array with mixed data. + * @param array $exclude + * (optional) An array with mixed data to exclude from the return. + * + * An array of used combinations, not used combinations and their number, + * last variation, variation attributes ids and options: + * - "last_variation": The variation on the last inline entity form array. + * - "used_combinations": The already used combinations. + * - "not_used_combinations": Yet not used combinations. + * - "count": The number of all combinations. + * - "attributes": An array with attributes ids and options: + * - "ids": The array of field_name => id pairs. + * - "options": The array of id => field_label pairs. + * - "duplicated": The number of duplicated combinations. + * - "used": The number of used combinations. + * combinations. + */ + public function getArrayValueCombinations(array $data = [], array $exclude = [], &$all = [], $group = [], $value = NULL, $i = 0, $k = NULL, $c = NULL, $f = NULL); + + /** + * Gets the IDs of the variation's attribute fields. + * + * @param \Drupal\commerce_product\Entity\ProductVariation $variation + * The commerce product variation. + * + * @return array + * An array of IDs arrays keyed by field name. + */ + public function getAttributeFieldOptionIds(ProductVariation $variation); + + /** + * Gets the names of the entity's attribute fields. + * + * @param \Drupal\commerce_product\Entity\ProductVariation $variation + * The commerce product variation. + * + * @return string[] + * The attribute field names. + */ + public function getAttributeFieldNames(ProductVariation $variation); + +} diff --git a/src/Plugin/Field/FieldWidget/ProductVariationSkuWidget.php b/src/Plugin/Field/FieldWidget/ProductVariationSkuWidget.php new file mode 100644 index 0000000000..fca7cb0ba2 --- /dev/null +++ b/src/Plugin/Field/FieldWidget/ProductVariationSkuWidget.php @@ -0,0 +1,144 @@ + '', + 'uniqid_enabled' => TRUE, + 'more_entropy' => FALSE, + 'hide' => FALSE, + 'prefix' => 'sku-', + 'suffix' => '', + 'size' => 60, + 'placeholder' => '', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $none = $this->t('None'); + $settings = $this->getSettings(); + $element['custom_label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Custom label'), + '#description' => $this->t('The label for the SKU field displayed on a variation edit form.'), + '#default_value' => empty($settings['custom_label']) ? '' : $settings['custom_label'], + '#placeholder' => $none, + ]; + $element['uniqid_enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable unique auto SKU values generation'), + '#default_value' => $settings['uniqid_enabled'], + ]; + $element['more_entropy'] = [ + '#type' => 'checkbox', + '#title_display' => 'before', + '#title' => $this->t('More unique'), + '#description' => $this->t('If unchecked the SKU (without prefix and suffix) will look like this: @short. If checked, like this: @long. Read more', [':uniqid_href' => 'http://php.net/manual/en/function.uniqid.php', '@short' => uniqid(), '@long' => uniqid('', TRUE)]), + '#default_value' => $settings['more_entropy'], + '#states' => [ + 'visible' => [':input[name*="uniqid_enabled"]' => ['checked' => TRUE]], + ], + ]; + $element['hide'] = [ + '#type' => 'checkbox', + '#title_display' => 'before', + '#title' => $this->t('Hide SKU'), + '#description' => $this->t('Hide the SKU field on a product add/edit forms adding SKU values silently at the background.'), + '#default_value' => $settings['hide'], + '#states' => [ + 'visible' => [':input[name*="uniqid_enabled"]' => ['checked' => TRUE]], + ], + ]; + $element['prefix'] = [ + '#type' => 'textfield', + '#title' => $this->t('SKU prefix'), + '#default_value' => $settings['prefix'], + '#placeholder' => $none, + ]; + $element['suffix'] = [ + '#type' => 'textfield', + '#title' => $this->t('SKU suffix'), + '#default_value' => $settings['suffix'], + '#placeholder' => $none, + '#description' => $this->t('Note if you leave all the above settings empty some services will become unavailable. For example, Variation Bulk Creator will be disabled on a product add or edit form.'), + ]; + $element['size'] = [ + '#type' => 'number', + '#title' => $this->t('Size of SKU field'), + '#default_value' => $settings['size'], + '#required' => TRUE, + '#min' => 1, + ]; + $element['placeholder'] = [ + '#type' => 'textfield', + '#title' => $this->t('Placeholder'), + '#default_value' => $settings['placeholder'], + '#description' => $this->t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'), + '#placeholder' => $none, + ]; + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = []; + $none = $this->t('None'); + $settings = $this->getSettings(); + $sku = uniqid($settings['prefix'], $settings['more_entropy']) . $settings['suffix']; + $settings['auto SKU sample'] = $settings['uniqid_enabled'] ? $sku : $none; + $settings['hide'] = $settings['hide'] ? $this->t('Yes') : $this->t('No'); + unset($settings['uniqid_enabled'], $settings['more_entropy']); + foreach ($settings as $name => $value) { + $value = empty($settings[$name]) ? $none : $value; + $summary[] = "{$name}: {$value}"; + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $custom_label = $this->getSetting('custom_label'); + $element['#title'] = !empty($custom_label) ? $custom_label : $element['#title']; + + $element['value'] = $element + [ + '#type' => 'textfield', + '#default_value' => isset($items[$delta]->value) ? $items[$delta]->value : NULL, + '#size' => $this->getSetting('size'), + '#placeholder' => $this->getSetting('placeholder'), + '#maxlength' => $this->getFieldSetting('max_length'), + '#attributes' => ['class' => ['js-text-full', 'text-full']], + ]; + + return $element; + } + +}