diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php
index 2dc519dbf1540..fe120e9a179dd 100644
--- a/app/code/Magento/Bundle/Model/Product/Type.php
+++ b/app/code/Magento/Bundle/Model/Product/Type.php
@@ -6,6 +6,8 @@
namespace Magento\Bundle\Model\Product;
+use Magento\Bundle\Model\Option;
+use Magento\Bundle\Model\ResourceModel\Option\Collection;
use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections;
use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier;
use Magento\Catalog\Api\ProductRepositoryInterface;
@@ -414,16 +416,13 @@ public function beforeSave($product)
if ($product->getCanSaveBundleSelections()) {
$product->canAffectOptions(true);
$selections = $product->getBundleSelectionsData();
- if ($selections && !empty($selections)) {
- $options = $product->getBundleOptionsData();
- if ($options) {
- foreach ($options as $option) {
- if (empty($option['delete']) || 1 != (int)$option['delete']) {
- $product->setTypeHasOptions(true);
- if (1 == (int)$option['required']) {
- $product->setTypeHasRequiredOptions(true);
- break;
- }
+ if (!empty($selections) && $options = $product->getBundleOptionsData()) {
+ foreach ($options as $option) {
+ if (empty($option['delete']) || 1 != (int)$option['delete']) {
+ $product->setTypeHasOptions(true);
+ if (1 == (int)$option['required']) {
+ $product->setTypeHasRequiredOptions(true);
+ break;
}
}
}
@@ -464,7 +463,7 @@ public function getOptionsIds($product)
public function getOptionsCollection($product)
{
if (!$product->hasData($this->_keyOptionsCollection)) {
- /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */
+ /** @var Collection $optionsCollection */
$optionsCollection = $this->_bundleOption->create()
->getResourceCollection();
$optionsCollection->setProductIdFilter($product->getEntityId());
@@ -530,10 +529,10 @@ public function getSelectionsCollection($optionIds, $product)
* Example: the catalog inventory validation of decimal qty can change qty to int,
* so need to change quote item qty option value too.
*
- * @param array $options
- * @param \Magento\Framework\DataObject $option
- * @param mixed $value
- * @param \Magento\Catalog\Model\Product $product
+ * @param array $options
+ * @param \Magento\Framework\DataObject $option
+ * @param mixed $value
+ * @param \Magento\Catalog\Model\Product $product
* @return $this
*/
public function updateQtyOption($options, \Magento\Framework\DataObject $option, $value, $product)
@@ -682,6 +681,11 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p
$options
);
+ $this->validateRadioAndSelectOptions(
+ $optionsCollection,
+ $options
+ );
+
$selectionIds = array_values($this->arrayUtility->flatten($options));
// If product has not been configured yet then $selections array should be empty
if (!empty($selectionIds)) {
@@ -1184,9 +1188,11 @@ public function canConfigure($product)
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
+ // @codingStandardsIgnoreStart
public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $product)
{
}
+ // @codingStandardsIgnoreEnd
/**
* Return array of specific to type product entities
@@ -1196,18 +1202,19 @@ public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $product)
*/
public function getIdentities(\Magento\Catalog\Model\Product $product)
{
- $identities = parent::getIdentities($product);
+ $identities = [];
+ $identities[] = parent::getIdentities($product);
/** @var \Magento\Bundle\Model\Option $option */
foreach ($this->getOptions($product) as $option) {
if ($option->getSelections()) {
/** @var \Magento\Catalog\Model\Product $selection */
foreach ($option->getSelections() as $selection) {
- $identities = array_merge($identities, $selection->getIdentities());
+ $identities[] = $selection->getIdentities();
}
}
}
- return $identities;
+ return array_merge([], ...$identities);
}
/**
@@ -1272,6 +1279,53 @@ protected function checkIsAllRequiredOptions($product, $isStrictProcessMode, $op
}
}
+ /**
+ * Validate Options for Radio and Select input types
+ *
+ * @param Collection $optionsCollection
+ * @param int[] $options
+ * @return void
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function validateRadioAndSelectOptions($optionsCollection, $options): void
+ {
+ $errorTypes = [];
+
+ if (is_array($optionsCollection->getItems())) {
+ foreach ($optionsCollection->getItems() as $option) {
+ if ($this->isSelectedOptionValid($option, $options)) {
+ $errorTypes[] = $option->getType();
+ }
+ }
+ }
+
+ if (!empty($errorTypes)) {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __(
+ 'Option type (%types) should have only one element.',
+ ['types' => implode(", ", $errorTypes)]
+ )
+ );
+ }
+ }
+
+ /**
+ * Check if selected option is valid
+ *
+ * @param Option $option
+ * @param array $options
+ * @return bool
+ */
+ private function isSelectedOptionValid($option, $options): bool
+ {
+ return (
+ ($option->getType() == 'radio' || $option->getType() == 'select') &&
+ isset($options[$option->getOptionId()]) &&
+ is_array($options[$option->getOptionId()]) &&
+ count($options[$option->getOptionId()]) > 1
+ );
+ }
+
/**
* Check if selection is salable
*
@@ -1333,16 +1387,18 @@ protected function checkIsResult($_result)
*/
protected function mergeSelectionsWithOptions($options, $selections)
{
+ $selections = [];
+
foreach ($options as $option) {
$optionSelections = $option->getSelections();
if ($option->getRequired() && is_array($optionSelections) && count($optionSelections) == 1) {
- $selections = array_merge($selections, $optionSelections);
+ $selections[] = $optionSelections;
} else {
$selections = [];
break;
}
}
- return $selections;
+ return array_merge([], ...$selections);
}
}
diff --git a/app/code/Magento/BundleGraphQl/Model/Order/Shipment/BundleShipmentItemFormatter.php b/app/code/Magento/BundleGraphQl/Model/Order/Shipment/BundleShipmentItemFormatter.php
new file mode 100644
index 0000000000000..8e678cdb12d24
--- /dev/null
+++ b/app/code/Magento/BundleGraphQl/Model/Order/Shipment/BundleShipmentItemFormatter.php
@@ -0,0 +1,51 @@
+itemFormatter = $itemFormatter;
+ }
+
+ /**
+ * Format bundle product shipment item
+ *
+ * @param ShipmentInterface $shipment
+ * @param ShipmentItemInterface $item
+ * @return array|null
+ */
+ public function formatShipmentItem(ShipmentInterface $shipment, ShipmentItemInterface $item): ?array
+ {
+ $orderItem = $item->getOrderItem();
+ $shippingType = $orderItem->getProductOptions()['shipment_type'] ?? null;
+ if ($shippingType == AbstractType::SHIPMENT_SEPARATELY && !$orderItem->getParentItemId()) {
+ //When bundle items are shipped separately the children are treated as their own items
+ return null;
+ }
+ return $this->itemFormatter->formatShipmentItem($shipment, $item);
+ }
+}
diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/BundleOptions.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php
similarity index 93%
rename from app/code/Magento/SalesGraphQl/Model/Resolver/BundleOptions.php
rename to app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php
index 0d27197e255ca..a21bbbb84d735 100644
--- a/app/code/Magento/SalesGraphQl/Model/Resolver/BundleOptions.php
+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php
@@ -5,7 +5,7 @@
*/
declare(strict_types=1);
-namespace Magento\SalesGraphQl\Model\Resolver;
+namespace Magento\BundleGraphQl\Model\Resolver\Order\Item;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
@@ -15,6 +15,8 @@
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Sales\Api\Data\InvoiceItemInterface;
use Magento\Sales\Api\Data\OrderItemInterface;
+use Magento\Sales\Api\Data\ShipmentItemInterface;
+use Magento\Sales\Api\Data\CreditmemoItemInterface;
/**
* Resolve bundle options items for order item
@@ -55,12 +57,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
throw new LocalizedException(__('"model" value should be specified'));
}
if ($value['model'] instanceof OrderItemInterface) {
- /** @var OrderItemInterface $item */
$item = $value['model'];
return $this->getBundleOptions($item, $value);
}
- if ($value['model'] instanceof InvoiceItemInterface) {
- /** @var InvoiceItemInterface $item */
+ if ($value['model'] instanceof InvoiceItemInterface
+ || $value['model'] instanceof ShipmentItemInterface
+ || $value['model'] instanceof CreditmemoItemInterface) {
$item = $value['model'];
// Have to pass down order and item to map to avoid refetching all data
return $this->getBundleOptions($item->getOrderItem(), $value);
diff --git a/app/code/Magento/BundleGraphQl/composer.json b/app/code/Magento/BundleGraphQl/composer.json
index cb49ab78588b3..e3c54719f4d0e 100644
--- a/app/code/Magento/BundleGraphQl/composer.json
+++ b/app/code/Magento/BundleGraphQl/composer.json
@@ -10,6 +10,8 @@
"magento/module-quote": "*",
"magento/module-quote-graph-ql": "*",
"magento/module-store": "*",
+ "magento/module-sales": "*",
+ "magento/module-sales-graph-ql": "*",
"magento/framework": "*"
},
"license": [
diff --git a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml
index b847a6672e046..863e152fbe177 100644
--- a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml
@@ -65,4 +65,39 @@
+