diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php
index cfa5ec91a2e1b..7aed842713f5d 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php
@@ -336,9 +336,10 @@ protected function _reindexRows($changedIds = [])
if (!empty($notCompositeIds)) {
$parentProductsTypes = $this->getParentProductsTypes($notCompositeIds);
$productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes);
- $parentProductsIds = array_keys($parentProductsTypes);
- $compositeIds = $compositeIds + array_combine($parentProductsIds, $parentProductsIds);
- $changedIds = array_merge($changedIds, $parentProductsIds);
+ foreach ($parentProductsTypes as $parentProductsIds) {
+ $compositeIds = $compositeIds + $parentProductsIds;
+ $changedIds = array_merge($changedIds, $parentProductsIds);
+ }
}
if (!empty($compositeIds)) {
@@ -370,7 +371,8 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null)
['child_id']
)->join(
['e' => $this->_defaultIndexerResource->getTable('catalog_product_entity')],
- 'e.' . $linkField . ' = parent_id'
+ 'e.' . $linkField . ' = parent_id',
+ []
)->where(
'e.entity_id IN(?)',
$parentIds
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php
index eb15833a7d0b2..ba04af8ec1f41 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php
@@ -109,7 +109,7 @@ public function execute($ids = null)
// Prepare replica table for indexation.
$this->_defaultIndexerResource->getConnection()->truncateTable($replicaTable);
- /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\AbstractIndexer $indexer */
+ /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $indexer */
foreach ($this->getTypeIndexers() as $indexer) {
$indexer->getTableStrategy()->setUseIdxTable(false);
$connection = $indexer->getConnection();
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php
index 591a26efbf615..285e1781e2f95 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php
@@ -52,6 +52,16 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface
*/
private $hasEntity = null;
+ /**
+ * @var IndexTableStructureFactory
+ */
+ private $indexTableStructureFactory;
+
+ /**
+ * @var PriceModifierInterface[]
+ */
+ private $priceModifiers = [];
+
/**
* DefaultPrice constructor.
*
@@ -61,7 +71,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface
* @param \Magento\Framework\Event\ManagerInterface $eventManager
* @param \Magento\Framework\Module\Manager $moduleManager
* @param string|null $connectionName
- * @param null|\Magento\Indexer\Model\Indexer\StateFactory $stateFactory
+ * @param null|IndexTableStructureFactory $indexTableStructureFactory
+ * @param PriceModifierInterface[] $priceModifiers
*/
public function __construct(
\Magento\Framework\Model\ResourceModel\Db\Context $context,
@@ -69,11 +80,25 @@ public function __construct(
\Magento\Eav\Model\Config $eavConfig,
\Magento\Framework\Event\ManagerInterface $eventManager,
\Magento\Framework\Module\Manager $moduleManager,
- $connectionName = null
+ $connectionName = null,
+ IndexTableStructureFactory $indexTableStructureFactory = null,
+ array $priceModifiers = []
) {
$this->_eventManager = $eventManager;
$this->moduleManager = $moduleManager;
parent::__construct($context, $tableStrategy, $eavConfig, $connectionName);
+
+ $this->indexTableStructureFactory = $indexTableStructureFactory ?:
+ \Magento\Framework\App\ObjectManager::getInstance()->get(IndexTableStructureFactory::class);
+ foreach ($priceModifiers as $priceModifier) {
+ if (!($priceModifier instanceof PriceModifierInterface)) {
+ throw new \InvalidArgumentException(
+ 'Argument \'priceModifiers\' must be of the type ' . PriceModifierInterface::class . '[]'
+ );
+ }
+
+ $this->priceModifiers[] = $priceModifier;
+ }
}
/**
@@ -209,6 +234,8 @@ protected function _getDefaultFinalPriceTable()
* Prepare final price temporary index table
*
* @return $this
+ * @deprecated
+ * @see prepareFinalPriceTable()
*/
protected function _prepareDefaultFinalPriceTable()
{
@@ -216,6 +243,32 @@ protected function _prepareDefaultFinalPriceTable()
return $this;
}
+ /**
+ * Create (if needed), clean and return structure of final price table
+ *
+ * @return IndexTableStructure
+ */
+ private function prepareFinalPriceTable()
+ {
+ $tableName = $this->_getDefaultFinalPriceTable();
+ $this->getConnection()->delete($tableName);
+
+ $finalPriceTable = $this->indexTableStructureFactory->create([
+ 'tableName' => $tableName,
+ 'entityField' => 'entity_id',
+ 'customerGroupField' => 'customer_group_id',
+ 'websiteField' => 'website_id',
+ 'taxClassField' => 'tax_class_id',
+ 'originalPriceField' => 'orig_price',
+ 'finalPriceField' => 'price',
+ 'minPriceField' => 'min_price',
+ 'maxPriceField' => 'max_price',
+ 'tierPriceField' => 'tier_price',
+ ]);
+
+ return $finalPriceTable;
+ }
+
/**
* Retrieve website current dates table name
*
@@ -248,11 +301,14 @@ protected function _prepareFinalPriceData($entityIds = null)
*/
protected function prepareFinalPriceDataForType($entityIds, $type)
{
- $this->_prepareDefaultFinalPriceTable();
+ $finalPriceTable = $this->prepareFinalPriceTable();
$select = $this->getSelect($entityIds, $type);
- $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false);
+ $query = $select->insertFromSelect($finalPriceTable->getTableName(), [], false);
$this->getConnection()->query($query);
+
+ $this->applyDiscountPrices($finalPriceTable);
+
return $this;
}
@@ -359,7 +415,7 @@ protected function getSelect($entityIds = null, $type = null)
'e.' . $metadata->getLinkField(),
'cs.store_id'
);
- $currentDate = $connection->getDatePartSql('cwd.website_date');
+ $currentDate = 'cwd.website_date';
$maxUnsignedBigint = '~0';
$specialFromDate = $connection->getDatePartSql($specialFrom);
@@ -409,6 +465,7 @@ protected function getSelect($entityIds = null, $type = null)
'store_field' => new \Zend_Db_Expr('cs.store_id'),
]
);
+
return $select;
}
@@ -454,6 +511,19 @@ protected function _prepareCustomOptionPriceTable()
return $this;
}
+ /**
+ * Apply discount prices to final price index table.
+ *
+ * @param IndexTableStructure $finalPriceTable
+ * @return void
+ */
+ private function applyDiscountPrices(IndexTableStructure $finalPriceTable) : void
+ {
+ foreach ($this->priceModifiers as $priceModifier) {
+ $priceModifier->modifyPrice($finalPriceTable);
+ }
+ }
+
/**
* Apply custom option minimal and maximal price to temporary final price index table
*
@@ -463,6 +533,7 @@ protected function _prepareCustomOptionPriceTable()
protected function _applyCustomOption()
{
$connection = $this->getConnection();
+ $finalPriceTable = $this->_getDefaultFinalPriceTable();
$coaTable = $this->_getCustomOptionAggregateTable();
$copTable = $this->_getCustomOptionPriceTable();
@@ -470,7 +541,7 @@ protected function _applyCustomOption()
$this->_prepareCustomOptionPriceTable();
$select = $connection->select()->from(
- ['i' => $this->_getDefaultFinalPriceTable()],
+ ['i' => $finalPriceTable],
['entity_id', 'customer_group_id', 'website_id']
)->join(
['cw' => $this->getTable('store_website')],
@@ -537,7 +608,7 @@ protected function _applyCustomOption()
$connection->query($query);
$select = $connection->select()->from(
- ['i' => $this->_getDefaultFinalPriceTable()],
+ ['i' => $finalPriceTable],
['entity_id', 'customer_group_id', 'website_id']
)->join(
['cw' => $this->getTable('store_website')],
@@ -606,7 +677,7 @@ protected function _applyCustomOption()
$query = $select->insertFromSelect($copTable);
$connection->query($query);
- $table = ['i' => $this->_getDefaultFinalPriceTable()];
+ $table = ['i' => $finalPriceTable];
$select = $connection->select()->join(
['io' => $copTable],
'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' .
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php
new file mode 100644
index 0000000000000..fb3eef2bf38eb
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php
@@ -0,0 +1,181 @@
+tableName = $tableName;
+ $this->entityField = $entityField;
+ $this->customerGroupField = $customerGroupField;
+ $this->websiteField = $websiteField;
+ $this->taxClassField = $taxClassField;
+ $this->originalPriceField = $originalPriceField;
+ $this->finalPriceField = $finalPriceField;
+ $this->minPriceField = $minPriceField;
+ $this->maxPriceField = $maxPriceField;
+ $this->tierPriceField = $tierPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTableName(): string
+ {
+ return $this->tableName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEntityField(): string
+ {
+ return $this->entityField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCustomerGroupField(): string
+ {
+ return $this->customerGroupField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getWebsiteField(): string
+ {
+ return $this->websiteField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTaxClassField(): string
+ {
+ return $this->taxClassField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOriginalPriceField(): string
+ {
+ return $this->originalPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFinalPriceField(): string
+ {
+ return $this->finalPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMinPriceField(): string
+ {
+ return $this->minPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMaxPriceField(): string
+ {
+ return $this->maxPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTierPriceField(): string
+ {
+ return $this->tierPriceField;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php
new file mode 100644
index 0000000000000..6ecb6aba89933
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php
@@ -0,0 +1,23 @@
+_eventManager = $eventManager;
$this->stockRegistry = $stockRegistry;
@@ -858,16 +865,14 @@ public function __construct(
$string,
$errorAggregator
);
- $this->_optionEntity = isset(
- $data['option_entity']
- ) ? $data['option_entity'] : $optionFactory->create(
- ['data' => ['product_entity' => $this]]
- );
+ $this->_optionEntity = $data['option_entity'] ??
+ $optionFactory->create(['data' => ['product_entity' => $this]]);
$this->_initAttributeSets()
->_initTypeModels()
->_initSkus()
->initImagesArrayKeys();
$this->validator->init($this);
+ $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class);
}
/**
@@ -2151,40 +2156,8 @@ protected function _saveStockItem()
$row = [];
$sku = $rowData[self::COL_SKU];
if ($this->skuProcessor->getNewSku($sku) !== null) {
- $row['product_id'] = $this->skuProcessor->getNewSku($sku)['entity_id'];
+ $row = $this->formatStockDataForRow($rowData);
$productIdsToReindex[] = $row['product_id'];
-
- $row['website_id'] = $this->stockConfiguration->getDefaultScopeId();
- $row['stock_id'] = $this->stockRegistry->getStock($row['website_id'])->getStockId();
-
- $stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']);
- $existStockData = $stockItemDo->getData();
-
- $row = array_merge(
- $this->defaultStockData,
- array_intersect_key($existStockData, $this->defaultStockData),
- array_intersect_key($rowData, $this->defaultStockData),
- $row
- );
- $row['sku'] = $sku;
-
- if ($this->stockConfiguration->isQty(
- $this->skuProcessor->getNewSku($sku)['type_id']
- )
- ) {
- $stockItemDo->setData($row);
- $row['is_in_stock'] = $this->stockStateProvider->verifyStock($stockItemDo);
- if ($this->stockStateProvider->verifyNotification($stockItemDo)) {
- $row['low_stock_date'] = gmdate(
- 'Y-m-d H:i:s',
- (new \DateTime())->getTimestamp()
- );
- }
- $row['stock_status_changed_auto'] =
- (int)!$this->stockStateProvider->verifyStock($stockItemDo);
- } else {
- $row['qty'] = 0;
- }
}
if (!isset($stockData[$sku])) {
@@ -2875,4 +2848,44 @@ private function getExistingSku($sku)
{
return $this->_oldSku[strtolower($sku)];
}
+
+ /**
+ * Format row data to DB compatible values.
+ *
+ * @param array $rowData
+ * @return array
+ */
+ private function formatStockDataForRow(array $rowData): array
+ {
+ $sku = $rowData[self::COL_SKU];
+ $row['product_id'] = $this->skuProcessor->getNewSku($sku)['entity_id'];
+ $row['website_id'] = $this->stockConfiguration->getDefaultScopeId();
+ $row['stock_id'] = $this->stockRegistry->getStock($row['website_id'])->getStockId();
+
+ $stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']);
+ $existStockData = $stockItemDo->getData();
+
+ $row = array_merge(
+ $this->defaultStockData,
+ array_intersect_key($existStockData, $this->defaultStockData),
+ array_intersect_key($rowData, $this->defaultStockData),
+ $row
+ );
+
+ if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) {
+ $stockItemDo->setData($row);
+ $row['is_in_stock'] = isset($row['is_in_stock']) && $stockItemDo->getBackorders()
+ ? $row['is_in_stock']
+ : $this->stockStateProvider->verifyStock($stockItemDo);
+ if ($this->stockStateProvider->verifyNotification($stockItemDo)) {
+ $date = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC'));
+ $row['low_stock_date'] = $date->format(DateTime::DATETIME_PHP_FORMAT);
+ }
+ $row['stock_status_changed_auto'] = (int)!$this->stockStateProvider->verifyStock($stockItemDo);
+ } else {
+ $row['qty'] = 0;
+ }
+
+ return $row;
+ }
}
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php
index cbaf401f32982..adb660dd118f9 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php
@@ -1223,6 +1223,7 @@ protected function _importData()
$typeTitles = [];
$parentCount = [];
$childCount = [];
+ $optionsToRemove = [];
foreach ($bunch as $rowNumber => $rowData) {
if (isset($optionId, $valueId) && empty($rowData[PRODUCT::COL_STORE_VIEW_CODE])) {
@@ -1232,14 +1233,18 @@ protected function _importData()
$optionId = $nextOptionId;
$valueId = $nextValueId;
$multiRowData = $this->_getMultiRowFormat($rowData);
-
+ if (!empty($rowData[self::COLUMN_SKU]) && isset($this->_productsSkuToId[$rowData[self::COLUMN_SKU]])) {
+ $this->_rowProductId = $this->_productsSkuToId[$rowData[self::COLUMN_SKU]];
+ if (array_key_exists('custom_options', $rowData) && trim($rowData['custom_options']) === '') {
+ $optionsToRemove[] = $this->_rowProductId;
+ }
+ }
foreach ($multiRowData as $optionData) {
$combinedData = array_merge($rowData, $optionData);
- if (!$this->isRowAllowedToImport($combinedData, $rowNumber)) {
- continue;
- }
- if (!$this->_parseRequiredData($combinedData)) {
+ if (!$this->isRowAllowedToImport($combinedData, $rowNumber)
+ || !$this->_parseRequiredData($combinedData)
+ ) {
continue;
}
$optionData = $this->_collectOptionMainData(
@@ -1266,38 +1271,45 @@ protected function _importData()
}
}
- // Save prepared custom options data !!!
- if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) {
- $this->_deleteEntities(array_keys($products));
- }
-
- if ($this->_isReadyForSaving($options, $titles, $typeValues)) {
- if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) {
- $this->_compareOptionsWithExisting($options, $titles, $prices, $typeValues);
- $this->restoreOriginalOptionTypeIds($typeValues, $typePrices, $typeTitles);
- }
+ $this->removeExistingOptions($products, $optionsToRemove);
- $this->_saveOptions(
- $options
- )->_saveTitles(
- $titles
- )->_savePrices(
- $prices
- )->_saveSpecificTypeValues(
- $typeValues
- )->_saveSpecificTypePrices(
- $typePrices
- )->_saveSpecificTypeTitles(
- $typeTitles
- )->_updateProducts(
- $products
- );
- }
+ $types = [
+ 'values' => $typeValues,
+ 'prices' => $typePrices,
+ 'titles' => $typeTitles,
+ ];
+ //Save prepared custom options data.
+ $this->savePreparedCustomOptions(
+ $products,
+ $options,
+ $titles,
+ $prices,
+ $types
+ );
}
return true;
}
+ /**
+ * Remove all existing options if import behaviour is APPEND
+ * in other case remove options for products with empty "custom_options" row only.
+ *
+ * @param array $products
+ * @param array $optionsToRemove
+ *
+ * @return void
+ */
+ private function removeExistingOptions(array $products, array $optionsToRemove): void
+ {
+ if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) {
+ $this->_deleteEntities(array_keys($products));
+ } elseif (!empty($optionsToRemove)) {
+ // Remove options for products with empty "custom_options" row
+ $this->_deleteEntities($optionsToRemove);
+ }
+ }
+
/**
* Load data of existed products
*
@@ -1537,9 +1549,7 @@ private function getExistingOptionTypeId($optionId, $storeId, $optionTypeTitle)
*/
protected function _parseRequiredData(array $rowData)
{
- if ($rowData[self::COLUMN_SKU] != '' && isset($this->_productsSkuToId[$rowData[self::COLUMN_SKU]])) {
- $this->_rowProductId = $this->_productsSkuToId[$rowData[self::COLUMN_SKU]];
- } elseif (!isset($this->_rowProductId)) {
+ if ($this->_rowProductId === null) {
return false;
}
@@ -1991,4 +2001,38 @@ private function getProductIdentifierField()
}
return $this->productEntityIdentifierField;
}
+
+ /**
+ * Save prepared custom options.
+ *
+ * @param array $products
+ * @param array $options
+ * @param array $titles
+ * @param array $prices
+ * @param array $types
+ *
+ * @return void
+ */
+ private function savePreparedCustomOptions(
+ array $products,
+ array $options,
+ array $titles,
+ array $prices,
+ array $types
+ ): void {
+ if ($this->_isReadyForSaving($options, $titles, $types['values'])) {
+ if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) {
+ $this->_compareOptionsWithExisting($options, $titles, $prices, $types['values']);
+ $this->restoreOriginalOptionTypeIds($types['values'], $types['prices'], $types['titles']);
+ }
+
+ $this->_saveOptions($options)
+ ->_saveTitles($titles)
+ ->_savePrices($prices)
+ ->_saveSpecificTypeValues($types['values'])
+ ->_saveSpecificTypePrices($types['prices'])
+ ->_saveSpecificTypeTitles($types['titles'])
+ ->_updateProducts($products);
+ }
+ }
}
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php b/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php
new file mode 100644
index 0000000000000..a60b05dc7c9bc
--- /dev/null
+++ b/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php
@@ -0,0 +1,75 @@
+priceResourceModel = $priceResourceModel;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = []) : void
+ {
+ $connection = $this->priceResourceModel->getConnection();
+ $select = $connection->select();
+
+ $select->join(
+ ['cpiw' => $this->priceResourceModel->getTable('catalog_product_index_website')],
+ 'cpiw.website_id = i.' . $priceTable->getWebsiteField(),
+ []
+ );
+ $select->join(
+ ['cpp' => $this->priceResourceModel->getMainTable()],
+ 'cpp.product_id = i.' . $priceTable->getEntityField()
+ . ' AND cpp.customer_group_id = i.' . $priceTable->getCustomerGroupField()
+ . ' AND cpp.website_id = i.' . $priceTable->getWebsiteField()
+ . ' AND cpp.rule_date = cpiw.website_date',
+ []
+ );
+ if ($entityIds) {
+ $select->where('i.entity_id IN (?)', $entityIds);
+ }
+
+ $finalPrice = $priceTable->getFinalPriceField();
+ $finalPriceExpr = $select->getConnection()->getLeastSql([
+ $priceTable->getFinalPriceField(),
+ $select->getConnection()->getIfNullSql('cpp.rule_price', 'i.' . $finalPrice),
+ ]);
+ $minPrice = $priceTable->getMinPriceField();
+ $minPriceExpr = $select->getConnection()->getLeastSql([
+ $priceTable->getMinPriceField(),
+ $select->getConnection()->getIfNullSql('cpp.rule_price', 'i.' . $minPrice),
+ ]);
+ $select->columns([
+ $finalPrice => $finalPriceExpr,
+ $minPrice => $minPriceExpr,
+ ]);
+
+ $query = $connection->updateFromSelect($select, ['i' => $priceTable->getTableName()]);
+ $connection->query($query);
+ }
+}
diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php
index 7696569cb26da..d927d6f4d0c82 100644
--- a/app/code/Magento/CatalogRule/Model/Rule.php
+++ b/app/code/Magento/CatalogRule/Model/Rule.php
@@ -606,7 +606,10 @@ public function afterSave()
*/
public function reindex()
{
- $this->_ruleProductProcessor->reindexList($this->_productIds);
+ $productIds = $this->_productIds ? array_keys(array_filter($this->_productIds, function (array $data) {
+ return array_filter($data);
+ })) : [];
+ $this->_ruleProductProcessor->reindexList($productIds);
}
/**
diff --git a/app/code/Magento/CatalogRule/etc/di.xml b/app/code/Magento/CatalogRule/etc/di.xml
index 40893592c3d0f..8ed88dd4f3fdb 100644
--- a/app/code/Magento/CatalogRule/etc/di.xml
+++ b/app/code/Magento/CatalogRule/etc/di.xml
@@ -150,4 +150,11 @@
+
+
+
+ - Magento\CatalogRule\Model\Indexer\ProductPriceIndexModifier
+
+
+
diff --git a/app/code/Magento/CatalogRule/etc/indexer.xml b/app/code/Magento/CatalogRule/etc/indexer.xml
index 08ed456457bfe..e648ea567631c 100644
--- a/app/code/Magento/CatalogRule/etc/indexer.xml
+++ b/app/code/Magento/CatalogRule/etc/indexer.xml
@@ -14,4 +14,9 @@
Catalog Product Rule
Indexed product/rule association
+
+
+
+
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Store/Block/Switcher.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Store/Block/Switcher.php
new file mode 100644
index 0000000000000..44213c007551c
--- /dev/null
+++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Store/Block/Switcher.php
@@ -0,0 +1,86 @@
+postHelper = $postHelper;
+ $this->urlFinder = $urlFinder;
+ $this->request = $request;
+ }
+
+ /**
+ * @param \Magento\Store\Block\Switcher $subject
+ * @param string $result
+ * @param Store $store
+ * @param array $data
+ * @return string
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterGetTargetStorePostData(
+ \Magento\Store\Block\Switcher $subject,
+ string $result,
+ Store $store,
+ array $data = []
+ ): string {
+ $data[StoreResolverInterface::PARAM_NAME] = $store->getCode();
+ $currentUrl = $store->getCurrentUrl(true);
+ $baseUrl = $store->getBaseUrl();
+ $urlPath = parse_url($currentUrl, PHP_URL_PATH);
+ $urlToSwitch = $currentUrl;
+
+ //check only catalog pages
+ if ($this->request->getFrontName() === 'catalog') {
+ $currentRewrite = $this->urlFinder->findOneByData([
+ UrlRewrite::REQUEST_PATH => ltrim($urlPath, '/'),
+ UrlRewrite::STORE_ID => $store->getId(),
+ ]);
+ if (null === $currentRewrite) {
+ $urlToSwitch = $baseUrl;
+ }
+ }
+
+ return $this->postHelper->getPostData($urlToSwitch, $data);
+ }
+}
diff --git a/app/code/Magento/CatalogUrlRewrite/etc/frontend/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/frontend/di.xml
new file mode 100644
index 0000000000000..3a9122b2f748d
--- /dev/null
+++ b/app/code/Magento/CatalogUrlRewrite/etc/frontend/di.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php
index 326310cc3c802..087931ebe5dcc 100644
--- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php
@@ -1,7 +1,5 @@
storeResolver = $storeResolver ?: \Magento\Framework\App\ObjectManager::getInstance()->get(
- StoreResolverInterface::class
- );
- }
-
/**
* @param null|int|array $entityIds
* @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable
@@ -58,6 +25,7 @@ protected function reindex($entityIds = null)
$this->_applyConfigurableOption($entityIds);
$this->_movePriceDataToIndexTable($entityIds);
}
+
return $this;
}
@@ -109,67 +77,49 @@ protected function _prepareConfigurableOptionPriceTable()
*
* @param array|null $entityIds
* @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
protected function _applyConfigurableOption($entityIds = null)
{
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
$connection = $this->getConnection();
- $coaTable = $this->_getConfigurableOptionAggregateTable();
$copTable = $this->_getConfigurableOptionPriceTable();
+ $finalPriceTable = $this->_getDefaultFinalPriceTable();
$linkField = $metadata->getLinkField();
- $this->_prepareConfigurableOptionAggregateTable();
$this->_prepareConfigurableOptionPriceTable();
- $subSelect = $this->getSelect();
- $subSelect->join(
+ $select = $connection->select()->from(
+ ['i' => $this->getIdxTable()],
+ []
+ )->join(
['l' => $this->getTable('catalog_product_super_link')],
- 'l.product_id = e.entity_id',
+ 'l.product_id = i.entity_id',
[]
)->join(
['le' => $this->getTable('catalog_product_entity')],
'le.' . $linkField . ' = l.parent_id',
- ['parent_id' => 'entity_id']
- );
-
- if ($entityIds !== null) {
- $subSelect->where('le.entity_id IN (?)', $entityIds);
- }
-
- $select = $connection->select();
- $select
- ->from(['sub' => new \Zend_Db_Expr('(' . (string)$subSelect . ')')], '')
- ->columns([
- 'sub.parent_id',
- 'sub.entity_id',
- 'sub.customer_group_id',
- 'sub.website_id',
- 'sub.price',
- 'sub.tier_price'
- ]);
-
- $query = $select->insertFromSelect($coaTable);
- $connection->query($query);
-
- $select = $connection->select()->from(
- [$coaTable],
+ []
+ )->columns(
[
- 'parent_id',
+ 'le.entity_id',
'customer_group_id',
'website_id',
- 'MIN(price)',
- 'MAX(price)',
+ 'MIN(final_price)',
+ 'MAX(final_price)',
'MIN(tier_price)',
+
]
)->group(
- ['parent_id', 'customer_group_id', 'website_id']
+ ['le.entity_id', 'customer_group_id', 'website_id']
);
+ if ($entityIds !== null) {
+ $select->where('le.entity_id IN (?)', $entityIds);
+ }
$query = $select->insertFromSelect($copTable);
$connection->query($query);
- $table = ['i' => $this->_getDefaultFinalPriceTable()];
+ $table = ['i' => $finalPriceTable];
$select = $connection->select()->join(
['io' => $copTable],
'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' .
@@ -188,7 +138,6 @@ protected function _applyConfigurableOption($entityIds = null)
$query = $select->crossUpdateFromSelect($table);
$connection->query($query);
- $connection->delete($coaTable);
$connection->delete($copTable);
return $this;
diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php
index dd83c28b43dff..855fac5041b21 100644
--- a/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php
+++ b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php
@@ -62,6 +62,7 @@ protected function _applyDownloadableLink()
{
$connection = $this->getConnection();
$table = $this->_getDownloadableLinkPriceTable();
+ $finalPriceTable = $this->_getDefaultFinalPriceTable();
$this->_prepareDownloadableLinkPriceTable();
@@ -71,7 +72,7 @@ protected function _applyDownloadableLink()
$ifPrice = $connection->getIfNullSql('dlpw.price_id', 'dlpd.price');
$select = $connection->select()->from(
- ['i' => $this->_getDefaultFinalPriceTable()],
+ ['i' => $finalPriceTable],
['entity_id', 'customer_group_id', 'website_id']
)->join(
['dl' => $dlType->getBackend()->getTable()],
@@ -119,7 +120,7 @@ protected function _applyDownloadableLink()
]
);
- $query = $select->crossUpdateFromSelect(['i' => $this->_getDefaultFinalPriceTable()]);
+ $query = $select->crossUpdateFromSelect(['i' => $finalPriceTable]);
$connection->query($query);
$connection->delete($table);
diff --git a/app/code/Magento/NewRelicReporting/Model/Observer/ReportApplicationHandledExceptionToNewRelic.php b/app/code/Magento/NewRelicReporting/Model/Observer/ReportApplicationHandledExceptionToNewRelic.php
index 724a488570207..ce7e95950c937 100644
--- a/app/code/Magento/NewRelicReporting/Model/Observer/ReportApplicationHandledExceptionToNewRelic.php
+++ b/app/code/Magento/NewRelicReporting/Model/Observer/ReportApplicationHandledExceptionToNewRelic.php
@@ -37,6 +37,9 @@ public function __construct(
$this->newRelicWrapper = $newRelicWrapper;
}
+ /**
+ * @param Observer $observer
+ */
public function execute(Observer $observer)
{
if ($this->config->isNewRelicEnabled()) {
diff --git a/app/code/Magento/Paypal/etc/adminhtml/system.xml b/app/code/Magento/Paypal/etc/adminhtml/system.xml
index 1c8da8127f8fe..c1ff4c9b1c6ca 100644
--- a/app/code/Magento/Paypal/etc/adminhtml/system.xml
+++ b/app/code/Magento/Paypal/etc/adminhtml/system.xml
@@ -32,6 +32,7 @@
+ 1
paypal-top-section payments-other-header
\Magento\Config\Block\System\Config\Form\Fieldset
diff --git a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php
index 1b781890e0f7f..80612277e68d5 100644
--- a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php
+++ b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php
@@ -123,10 +123,15 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object)
{
/** @var \Magento\Sales\Model\AbstractModel $object */
if ($object instanceof EntityInterface && $object->getIncrementId() == null) {
+ $store = $object->getStore();
+ $storeId = $store->getId();
+ if ($storeId === null) {
+ $storeId = $store->getGroup()->getDefaultStoreId();
+ }
$object->setIncrementId(
$this->sequenceManager->getSequence(
$object->getEntityType(),
- $object->getStore()->getGroup()->getDefaultStoreId()
+ $storeId
)->getNextValue()
);
}
diff --git a/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php b/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php
index 775a7dab95cfe..cd8c705750d6c 100644
--- a/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php
+++ b/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php
@@ -31,6 +31,6 @@ public function __construct(\Magento\Quote\Model\ResourceModel\Quote $quote)
public function execute(\Magento\Framework\Event\Observer $observer)
{
$product = $observer->getEvent()->getProduct();
- $this->_quote->substractProductFromQuotes($product);
+ $this->_quote->subtractProductFromQuotes($product);
}
}
diff --git a/app/code/Magento/Sales/Setup/SalesSetup.php b/app/code/Magento/Sales/Setup/SalesSetup.php
index bfc05c549ddb3..4be2b38b074e7 100644
--- a/app/code/Magento/Sales/Setup/SalesSetup.php
+++ b/app/code/Magento/Sales/Setup/SalesSetup.php
@@ -303,6 +303,9 @@ public function getEncryptor()
return $this->encryptor;
}
+ /**
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
public function getConnection()
{
return $this->getSetup()->getConnection(self::$connectionName);
diff --git a/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php b/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php
index a6a828c888fc0..949121eadee44 100644
--- a/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php
@@ -48,7 +48,7 @@ public function testSubtractQtyFromQuotes()
['getId', 'getStatus', '__wakeup']
);
$this->_eventMock->expects($this->once())->method('getProduct')->will($this->returnValue($productMock));
- $this->_quoteMock->expects($this->once())->method('substractProductFromQuotes')->with($productMock);
+ $this->_quoteMock->expects($this->once())->method('subtractProductFromQuotes')->with($productMock);
$this->_model->execute($this->_observerMock);
}
}
diff --git a/app/code/Magento/Tax/Model/Config.php b/app/code/Magento/Tax/Model/Config.php
index b30e5afb85142..09212ce90bf58 100644
--- a/app/code/Magento/Tax/Model/Config.php
+++ b/app/code/Magento/Tax/Model/Config.php
@@ -832,12 +832,12 @@ public function getInfoUrl($store = null)
* If it necessary will be returned conversion type (minus or plus)
*
* @param null|int|string|Store $store
- * @return bool
+ * @return bool|int
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function needPriceConversion($store = null)
{
- $res = false;
+ $res = 0;
$priceIncludesTax = $this->priceIncludesTax($store) || $this->getNeedUseShippingExcludeTax();
if ($priceIncludesTax) {
switch ($this->getPriceDisplayType($store)) {
@@ -845,7 +845,7 @@ public function needPriceConversion($store = null)
case self::DISPLAY_TYPE_BOTH:
return self::PRICE_CONVERSION_MINUS;
case self::DISPLAY_TYPE_INCLUDING_TAX:
- $res = true;
+ $res = false;
break;
default:
break;
diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule.php
index e509f477bcd8d..91fd0f4dcffb3 100644
--- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule.php
+++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule.php
@@ -6,7 +6,7 @@
namespace Magento\Tax\Model\ResourceModel\Calculation;
/**
- * Tax rate resource model
+ * Tax rule resource model
*
* @author Magento Core Team
*/
diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less
index 3136056d9426d..c7fa7a62fd68e 100644
--- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less
+++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less
@@ -46,6 +46,11 @@
.lib-css(border-top, @checkout-payment-method-title__border);
}
}
+ form {
+ &.form-purchase-order {
+ margin-bottom: 15px;
+ }
+ }
}
.payment-method-content {
diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Paypal/Page/AdminConfigPaymentMethodsPage.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Paypal/Page/AdminConfigPaymentMethodsPage.xml
new file mode 100644
index 0000000000000..9fa1a71e3441b
--- /dev/null
+++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Paypal/Page/AdminConfigPaymentMethodsPage.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Paypal/Section/OtherPaymentsConfigSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Paypal/Section/OtherPaymentsConfigSection.xml
new file mode 100644
index 0000000000000..024c1317dba2f
--- /dev/null
+++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Paypal/Section/OtherPaymentsConfigSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Paypal/Test/AdminConfigPaymentsSectionState.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Paypal/Test/AdminConfigPaymentsSectionState.xml
new file mode 100644
index 0000000000000..90c8dd5896198
--- /dev/null
+++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Paypal/Test/AdminConfigPaymentsSectionState.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php
index bd83cdcaf3fb1..88ac682f6b282 100644
--- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php
+++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php
@@ -100,6 +100,7 @@ protected function _getFixtures($scope, \PHPUnit\Framework\TestCase $test)
* Execute single fixture script
*
* @param string|array $fixture
+ * @throws \Throwable
*/
protected function _applyOneFixture($fixture)
{
@@ -110,9 +111,13 @@ protected function _applyOneFixture($fixture)
require $fixture;
}
} catch (\Exception $e) {
- echo 'Exception occurred when running the '
- . (is_array($fixture) || is_scalar($fixture) ? json_encode($fixture) : 'callback')
- . ' fixture: ', PHP_EOL, $e;
+ throw new \Exception(
+ sprintf(
+ "Exception occurred when running the %s fixture: \n%s",
+ (\is_array($fixture) || is_scalar($fixture) ? json_encode($fixture) : 'callback'),
+ $e->getMessage()
+ )
+ );
}
$this->_appliedFixtures[] = $fixture;
}
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Edit/JsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Edit/JsTest.php
index 79989c24ea96d..f2043b3c5fff1 100644
--- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Edit/JsTest.php
+++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Edit/JsTest.php
@@ -29,23 +29,18 @@ public function testGetAllRatesByProductClassJson()
/** @var \Magento\Catalog\Block\Adminhtml\Product\Edit\Js $block */
$block = $objectManager->create(\Magento\Catalog\Block\Adminhtml\Product\Edit\Js::class);
$jsonResult = $block->getAllRatesByProductClassJson();
- $decodedResult = json_decode($jsonResult);
- $this->assertNotEmpty($decodedResult, 'Resulting JSON is invalid.');
- $taxClassesArray = (array)$decodedResult;
+ $this->assertJson($jsonResult, 'Resulting JSON is invalid.');
+ $decodedResult = json_decode($jsonResult, true);
+ $this->assertNotNull($decodedResult, 'Cannot decode resulting JSON.');
$noneTaxClass = 0;
$defaultProductTaxClass = 2;
$expectedProductTaxClasses = array_unique(
array_merge($fixtureTaxRule->getProductTaxClasses(), [$defaultProductTaxClass, $noneTaxClass])
);
- $this->assertCount(
- count($expectedProductTaxClasses),
- $taxClassesArray,
- 'Invalid quantity of rates for tax classes.'
- );
foreach ($expectedProductTaxClasses as $taxClassId) {
$this->assertArrayHasKey(
"value_{$taxClassId}",
- $taxClassesArray,
+ $decodedResult,
"Rates for tax class with ID '{$taxClassId}' is missing."
);
}
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php
index 8b33c962cc809..b00090850e09b 100644
--- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php
+++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php
@@ -4,6 +4,8 @@
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Model;
use Magento\Framework\App\Filesystem\DirectoryList;
@@ -552,4 +554,47 @@ public function testGetOptions()
}
}
}
+
+ /**
+ * Check stock status changing if backorders functionality enabled.
+ *
+ * @magentoDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php
+ * @dataProvider productWithBackordersDataProvider
+ * @param int $qty
+ * @param int $stockStatus
+ * @param bool $expectedStockStatus
+ *
+ * @return void
+ */
+ public function testSaveWithBackordersEnabled(int $qty, int $stockStatus, bool $expectedStockStatus): void
+ {
+ $product = $this->productRepository->get('simple-out-of-stock', true, null, true);
+ $stockItem = $product->getExtensionAttributes()->getStockItem();
+ $this->assertEquals(false, $stockItem->getIsInStock());
+ $stockData = [
+ 'backorders' => 1,
+ 'qty' => $qty,
+ 'is_in_stock' => $stockStatus,
+ ];
+ $product->setStockData($stockData);
+ $product->save();
+ $stockItem = $product->getExtensionAttributes()->getStockItem();
+
+ $this->assertEquals($expectedStockStatus, $stockItem->getIsInStock());
+ }
+
+ /**
+ * DataProvider for the testSaveWithBackordersEnabled()
+ * @return array
+ */
+ public function productWithBackordersDataProvider(): array
+ {
+ return [
+ [0, 0, false],
+ [0, 1, true],
+ [-1, 0, false],
+ [-1, 1, true],
+ [1, 1, true],
+ ];
+ }
}
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock.php
new file mode 100644
index 0000000000000..6630c0d69e34f
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock.php
@@ -0,0 +1,53 @@
+reinitialize();
+
+/** @var \Magento\TestFramework\ObjectManager $objectManager */
+$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
+
+/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */
+$categoryLinkManagement = $objectManager->get(\Magento\Catalog\Api\CategoryLinkManagementInterface::class);
+
+/** @var $product \Magento\Catalog\Model\Product */
+$product = $objectManager->create(\Magento\Catalog\Model\Product::class);
+$product->isObjectNew(true);
+$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)
+ ->setAttributeSetId($product->getDefaultAttributeSetId())
+ ->setWebsiteIds([1])
+ ->setName('Simple Product')
+ ->setSku('simple-out-of-stock')
+ ->setPrice(10)
+ ->setWeight(1)
+ ->setShortDescription("Short description")
+ ->setTaxClassId(0)
+ ->setDescription('Description with html tag')
+ ->setMetaTitle('meta title')
+ ->setMetaKeyword('meta keyword')
+ ->setMetaDescription('meta description')
+ ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
+ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
+ ->setStockData(
+ [
+ 'use_config_manage_stock' => 1,
+ 'qty' => 0,
+ 'is_qty_decimal' => 0,
+ 'is_in_stock' => 0,
+ ]
+ )
+ ->setCanSaveCustomOptions(true)
+ ->setHasOptions(true);
+
+/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */
+$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+$productRepository->save($product);
+
+$categoryLinkManagement->assignProductToCategories(
+ $product->getSku(),
+ [2]
+);
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_rollback.php
new file mode 100644
index 0000000000000..c4df4454aa07c
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_rollback.php
@@ -0,0 +1,28 @@
+getInstance()->reinitialize();
+
+/** @var \Magento\Framework\Registry $registry */
+$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
+$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+try {
+ $product = $productRepository->get('simple-out-of-stock', false, null, true);
+ $productRepository->delete($product);
+} catch (NoSuchEntityException $e) {
+}
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php
index 27638f4b092dd..c5e704c2434b5 100644
--- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php
+++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php
@@ -271,15 +271,18 @@ public function testStockState()
}
/**
- * Tests adding of custom options with existing and new product
+ * Tests adding of custom options with existing and new product.
*
* @magentoDataFixture Magento/Catalog/_files/product_simple.php
* @dataProvider getBehaviorDataProvider
* @param string $importFile
* @param string $sku
+ * @param int $expectedOptionsQty
* @magentoAppIsolation enabled
+ *
+ * @return void
*/
- public function testSaveCustomOptions($importFile, $sku)
+ public function testSaveCustomOptions(string $importFile, string $sku, int $expectedOptionsQty): void
{
$pathToFile = __DIR__ . '/_files/' . $importFile;
$importModel = $this->createImportModel($pathToFile);
@@ -312,6 +315,7 @@ public function testSaveCustomOptions($importFile, $sku)
// assert of options data
$this->assertCount(count($expectedData['data']), $actualData['data']);
$this->assertCount(count($expectedData['values']), $actualData['values']);
+ $this->assertCount($expectedOptionsQty, $actualData['options']);
foreach ($expectedData['options'] as $expectedId => $expectedOption) {
$elementExist = false;
// find value in actual options and values
@@ -411,17 +415,24 @@ public function testSaveCustomOptionsWithMultipleStoreViews()
/**
* @return array
*/
- public function getBehaviorDataProvider()
+ public function getBehaviorDataProvider(): array
{
return [
'Append behavior with existing product' => [
- '$importFile' => 'product_with_custom_options.csv',
- '$sku' => 'simple',
+ 'importFile' => 'product_with_custom_options.csv',
+ 'sku' => 'simple',
+ 'expectedOptionsQty' => 6,
+ ],
+ 'Append behavior with existing product and without options in import file' => [
+ 'importFile' => 'product_without_custom_options.csv',
+ 'sku' => 'simple',
+ 'expectedOptionsQty' => 0,
],
'Append behavior with new product' => [
- '$importFile' => 'product_with_custom_options_new.csv',
- '$sku' => 'simple_new',
- ]
+ 'importFile' => 'product_with_custom_options_new.csv',
+ 'sku' => 'simple_new',
+ 'expectedOptionsQty' => 4,
+ ],
];
}
@@ -571,43 +582,45 @@ protected function getExpectedOptionsData(string $pathToFile, string $storeCode
break;
}
}
- foreach (explode('|', $productData['data'][$storeRowId]['custom_options']) as $optionData) {
- $option = array_values(
- array_map(
- function ($input) {
- $data = explode('=', $input);
- return [$data[0] => $data[1]];
- },
- explode(',', $optionData)
- )
- );
- $option = array_merge(...$option);
-
- if (!empty($option['type']) && !empty($option['name'])) {
- $lastOptionKey = $option['type'] . '|' . $option['name'];
- if (!isset($expectedOptions[$expectedOptionId])
- || $expectedOptions[$expectedOptionId] != $lastOptionKey) {
- $expectedOptionId++;
- $expectedOptions[$expectedOptionId] = $lastOptionKey;
- $expectedData[$expectedOptionId] = [];
- foreach ($this->_assertOptions as $assertKey => $assertFieldName) {
- if (array_key_exists($assertFieldName, $option)
- && !(($assertFieldName == 'price' || $assertFieldName == 'sku')
- && in_array($option['type'], $this->specificTypes))
- ) {
- $expectedData[$expectedOptionId][$assertKey] = $option[$assertFieldName];
+ if (!empty($productData['data'][$storeRowId]['custom_options'])) {
+ foreach (explode('|', $productData['data'][$storeRowId]['custom_options']) as $optionData) {
+ $option = array_values(
+ array_map(
+ function ($input) {
+ $data = explode('=', $input);
+ return [$data[0] => $data[1]];
+ },
+ explode(',', $optionData)
+ )
+ );
+ $option = array_merge(...$option);
+
+ if (!empty($option['type']) && !empty($option['name'])) {
+ $lastOptionKey = $option['type'] . '|' . $option['name'];
+ if (!isset($expectedOptions[$expectedOptionId])
+ || $expectedOptions[$expectedOptionId] != $lastOptionKey) {
+ $expectedOptionId++;
+ $expectedOptions[$expectedOptionId] = $lastOptionKey;
+ $expectedData[$expectedOptionId] = [];
+ foreach ($this->_assertOptions as $assertKey => $assertFieldName) {
+ if (array_key_exists($assertFieldName, $option)
+ && !(($assertFieldName == 'price' || $assertFieldName == 'sku')
+ && in_array($option['type'], $this->specificTypes))
+ ) {
+ $expectedData[$expectedOptionId][$assertKey] = $option[$assertFieldName];
+ }
}
}
}
- }
- $optionValue = [];
- if (!empty($option['name']) && !empty($option['option_title'])) {
- foreach ($this->_assertOptionValues as $assertKey => $assertFieldName) {
- if (isset($option[$assertFieldName])) {
- $optionValue[$assertKey] = $option[$assertFieldName];
+ $optionValue = [];
+ if (!empty($option['name']) && !empty($option['option_title'])) {
+ foreach ($this->_assertOptionValues as $assertKey => $assertFieldName) {
+ if (isset($option[$assertFieldName])) {
+ $optionValue[$assertKey] = $option[$assertFieldName];
+ }
}
+ $expectedValues[$expectedOptionId][] = $optionValue;
}
- $expectedValues[$expectedOptionId][] = $optionValue;
}
}
@@ -2181,4 +2194,51 @@ public function testProductsWithMultipleStoresWhenMediaIsDisabled(): void
$this->assertTrue($errors->getErrorsCount() === 0);
$this->assertTrue($this->_model->importData());
}
+
+ /**
+ * Test that imported product stock status with backorders functionality enabled can be set to 'out of stock'.
+ *
+ * @magentoDbIsolation enabled
+ * @magentoAppIsolation enabled
+ *
+ * @return void
+ */
+ public function testImportWithBackordersEnabled(): void
+ {
+ $this->importFile('products_to_import_with_backorders_enabled_and_0_qty.csv');
+ $product = $this->getProductBySku('simple_new');
+ $this->assertFalse($product->getDataByKey('quantity_and_stock_status')['is_in_stock']);
+ }
+
+ /**
+ * Import file by providing import filename in parameters.
+ *
+ * @param string $fileName
+ * @return void
+ */
+ private function importFile(string $fileName): void
+ {
+ $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class);
+ $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT);
+ $source = $this->objectManager->create(
+ \Magento\ImportExport\Model\Import\Source\Csv::class,
+ [
+ 'file' => __DIR__ . '/_files/' . $fileName,
+ 'directory' => $directory,
+ ]
+ );
+ $errors = $this->_model->setParameters(
+ [
+ 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND,
+ 'entity' => 'catalog_product',
+ \Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE => 1,
+ ]
+ )
+ ->setSource($source)
+ ->validateData();
+
+ $this->assertTrue($errors->getErrorsCount() == 0);
+
+ $this->_model->importData();
+ }
}
diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_without_custom_options.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_without_custom_options.csv
new file mode 100644
index 0000000000000..17bffb427a0e9
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_without_custom_options.csv
@@ -0,0 +1,2 @@
+sku,website_code,store_view_code,attribute_set_code,product_type,name,description,short_description,weight,product_online,visibility,product_websites,categories,price,special_price,special_price_from_date,special_price_to_date,tax_class_name,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,additional_images,additional_image_labels,configurable_variation_labels,configurable_variations,bundle_price_type,bundle_sku_type,bundle_weight_type,bundle_values,downloadble_samples,downloadble_links,associated_skus,related_skus,crosssell_skus,upsell_skus,custom_options,additional_attributes,manage_stock,is_in_stock,qty,out_of_stock_qty,is_qty_decimal,allow_backorders,min_cart_qty,max_cart_qty,notify_on_stock_below,qty_increments,enable_qty_increments,is_decimal_divided,new_from_date,new_to_date,gift_message_available,created_at,updated_at,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_price,msrp_display_actual_price_type,map_enabled
+simple,base,,Default,simple,New Product,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product,,,,,,,,,,,,,,,,,,,,,,,,,,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,,
diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_enabled_and_0_qty.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_enabled_and_0_qty.csv
new file mode 100644
index 0000000000000..e2107766c37bd
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_enabled_and_0_qty.csv
@@ -0,0 +1,2 @@
+sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus
+simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product ,,,,,,,10/20/2015 7:05,10/20/2015 7:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",0,0,1,0,1,0,1,1,10000,1,0,1,1,1,0,1,1,0,0,0,1,,,,,,,,,,,,,
diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php
new file mode 100644
index 0000000000000..b1a10c894f83a
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php
@@ -0,0 +1,92 @@
+resourceRule = Bootstrap::getObjectManager()->get(Rule::class);
+ }
+
+ /**
+ * @return void
+ */
+ public function testPriceApplying() : void
+ {
+ $customerGroupId = 1;
+ $websiteId = 1;
+
+ $simpleProductId = 1;
+ $collection = Bootstrap::getObjectManager()->create(Collection::class);
+ $collection->addIdFilter($simpleProductId);
+ $collection->addPriceData($customerGroupId, $websiteId);
+ $collection->load();
+ /** @var \Magento\Catalog\Model\Product $simpleProduct */
+ $simpleProduct = $collection->getFirstItem();
+ $simpleProduct->setPriceCalculation(false);
+
+ $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, $customerGroupId, $simpleProductId);
+ $this->assertEquals($rulePrice, $simpleProduct->getFinalPrice());
+
+ $confProductId = 666;
+ $collection = Bootstrap::getObjectManager()->create(Collection::class);
+ $collection->addIdFilter($confProductId);
+ $collection->addPriceData($customerGroupId, $websiteId);
+ $collection->load();
+ /** @var \Magento\Catalog\Model\Product $confProduct */
+ $confProduct = $collection->getFirstItem();
+
+ $this->assertEquals($simpleProduct->getMinimalPrice(), $confProduct->getMinimalPrice());
+ }
+
+ /**
+ * @magentoAppArea frontend
+ *
+ * @return void
+ */
+ public function testSortByPrice() : void
+ {
+ $searchCriteria = Bootstrap::getObjectManager()->create(SearchCriteriaInterface::class);
+ $sortOrder = Bootstrap::getObjectManager()->create(SortOrder::class);
+ $sortOrder->setField('price')->setDirection(SortOrder::SORT_ASC);
+ $searchCriteria->setSortOrders([$sortOrder]);
+ $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class);
+ $searchResults = $productRepository->getList($searchCriteria);
+ $products = $searchResults->getItems();
+
+ /** @var \Magento\Catalog\Model\Product $product1 */
+ $product1 = array_values($products)[0];
+ $product1->setPriceCalculation(false);
+ $this->assertEquals('simple1', $product1->getSku());
+ $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), 1, 1, $product1->getId());
+ $this->assertEquals($rulePrice, $product1->getFinalPrice());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/RuleProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/RuleProductTest.php
index c8c0726de7ebe..a4a99918fe052 100644
--- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/RuleProductTest.php
+++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/RuleProductTest.php
@@ -40,7 +40,7 @@ protected function setUp()
public function testReindexAfterRuleCreation()
{
/** @var \Magento\Catalog\Model\ProductRepository $productRepository */
- $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
+ $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(
\Magento\Catalog\Model\ProductRepository::class
);
$product = $productRepository->get('simple');
diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_attribute.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_attribute.php
new file mode 100644
index 0000000000000..071f5d7d9fd00
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_attribute.php
@@ -0,0 +1,108 @@
+get(\Magento\Store\Model\StoreManager::class);
+$store = $storeManager->getStore('default');
+
+$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+
+$installer = $objectManager->get(\Magento\Catalog\Setup\CategorySetup::class);
+$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default');
+$attributeValues = [];
+$associatedProductIds = [];
+
+/** @var Magento\Eav\Model\Entity\Attribute\Option[] $options */
+$options = $attribute->getOptions();
+array_shift($options); //remove the first option which is empty
+
+$product = $objectManager->create(\Magento\Catalog\Model\Product::class)
+ ->setTypeId('simple')
+ ->setId(1)
+ ->setAttributeSetId($attributeSetId)
+ ->setWebsiteIds([1])
+ ->setName('Simple Product 1')
+ ->setSku('simple1')
+ ->setPrice(10)
+ ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
+ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
+ ->setStockData([
+ 'use_config_manage_stock' => 1,
+ 'qty' => 100,
+ 'is_qty_decimal' => 0,
+ 'is_in_stock' => 1,
+ ]);
+$option = array_shift($options);
+$product->setTestConfigurable($option->getValue());
+$productRepository->save($product);
+$attributeValues[] = [
+ 'label' => 'test',
+ 'attribute_id' => $attribute->getId(),
+ 'value_index' => $option->getValue(),
+];
+$associatedProductIds[] = $product->getId();
+$productAction = $objectManager->get(\Magento\Catalog\Model\Product\Action::class);
+$productAction->updateAttributes([$product->getId()], ['test_attribute' => 'test_attribute_value'], $store->getId());
+
+$product = $objectManager->create(\Magento\Catalog\Model\Product::class)
+ ->setTypeId('simple')
+ ->setId(2)
+ ->setAttributeSetId($attributeSetId)
+ ->setWebsiteIds([1])
+ ->setName('Simple Product 2')
+ ->setSku('simple2')
+ ->setPrice(9.9)
+ ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
+ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
+ ->setStockData([
+ 'use_config_manage_stock' => 1,
+ 'qty' => 100,
+ 'is_qty_decimal' => 0,
+ 'is_in_stock' => 1,
+ ]);
+$option = array_shift($options);
+$product->setTestConfigurable($option->getValue());
+$productRepository->save($product);
+$attributeValues[] = [
+ 'label' => 'test',
+ 'attribute_id' => $attribute->getId(),
+ 'value_index' => $option->getValue(),
+];
+$associatedProductIds[] = $product->getId();
+
+$product = $objectManager->create(\Magento\Catalog\Model\Product::class)
+ ->setTypeId('configurable')
+ ->setId(666)
+ ->setAttributeSetId($attributeSetId)
+ ->setWebsiteIds([1])
+ ->setName('Configurable Product')
+ ->setSku('configurable')
+ ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
+ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
+ ->setStockData([
+ 'use_config_manage_stock' => 1,
+ 'is_in_stock' => 0,
+ ]);
+$configurableAttributesData = [
+ [
+ 'attribute_id' => $attribute->getId(),
+ 'code' => $attribute->getAttributeCode(),
+ 'label' => $attribute->getStoreLabel(),
+ 'position' => '0',
+ 'values' => $attributeValues,
+ ],
+];
+$optionsFactory = $objectManager->get(\Magento\ConfigurableProduct\Helper\Product\Options\Factory::class);
+$configurableOptions = $optionsFactory->create($configurableAttributesData);
+$extensionConfigurableAttributes = $product->getExtensionAttributes();
+$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions);
+$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds);
+$product->setExtensionAttributes($extensionConfigurableAttributes);
+$productRepository->save($product);
diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_attribute_rollback.php
new file mode 100644
index 0000000000000..0ce909a3f9ecd
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_attribute_rollback.php
@@ -0,0 +1,33 @@
+getInstance()->reinitialize();
+
+/** @var $objectManager \Magento\TestFramework\ObjectManager */
+$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
+
+/** @var \Magento\Framework\Registry $registry */
+$registry = $objectManager->get(\Magento\Framework\Registry::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
+$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+foreach (['simple1', 'simple2', 'configurable'] as $sku) {
+ try {
+ $product = $productRepository->get($sku, false, null, true);
+ $productRepository->delete($product);
+ } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
+ //Nothing to delete
+ }
+}
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
+
+require __DIR__ . '/../../ConfigurableProduct/_files/configurable_attribute_rollback.php';
diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Store/Block/SwitcherTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Store/Block/SwitcherTest.php
new file mode 100644
index 0000000000000..5b3879b592245
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Store/Block/SwitcherTest.php
@@ -0,0 +1,56 @@
+objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
+ $this->model = $this->objectManager->create(\Magento\Store\Block\Switcher::class);
+ $this->storeRepository = $this->objectManager->create(\Magento\Store\Api\StoreRepositoryInterface::class);
+ }
+
+ /**
+ * Test that after switching from Store 1 to Store 2 with another root Category user gets correct store url.
+ *
+ * @magentoDataFixture Magento/Store/_files/store.php
+ * @magentoDataFixture Magento/CatalogUrlRewrite/_files/two_categories_per_two_store_groups.php
+ * @magentoAppArea frontend
+ * @return void
+ */
+ public function testGetTargetStorePostData(): void
+ {
+ $storeCode = 'test';
+ $store = $this->storeRepository->get($storeCode);
+ $result = json_decode($this->model->getTargetStorePostData($store), true);
+
+ $this->assertContains($storeCode, $result['action']);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/two_categories_per_two_store_groups.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/two_categories_per_two_store_groups.php
new file mode 100644
index 0000000000000..90a74351d8200
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/two_categories_per_two_store_groups.php
@@ -0,0 +1,70 @@
+create(\Magento\Catalog\Helper\DefaultCategory::class);
+/** @var \Magento\Catalog\Model\Category $category */
+$category = $objectManager->create(\Magento\Catalog\Model\Category::class);
+$category->isObjectNew(true);
+$category->setCreatedAt('2014-06-23 09:50:07')
+ ->setName('Category 1')
+ ->setParentId($defaultCategory->getId())
+ ->setPath('1/2/3')
+ ->setLevel(2)
+ ->setAvailableSortBy('name')
+ ->setDefaultSortBy('name')
+ ->setIsActive(true)
+ ->setPosition(1)
+ ->setAvailableSortBy(['position'])
+ ->save();
+
+/** @var \Magento\Store\Model\Store $store */
+$store = $objectManager->create(\Magento\Store\Model\Store::class);
+
+$category->setStoreId($store->load('default')->getId())
+ ->setName('category-default-store')
+ ->setUrlKey('category-default-store')
+ ->save();
+
+$rootCategoryForTestStoreGroup = $objectManager->create(\Magento\Catalog\Model\Category::class);
+$rootCategoryForTestStoreGroup->isObjectNew(true);
+$rootCategoryForTestStoreGroup->setCreatedAt('2014-06-23 09:50:07')
+ ->setName('Category 2')
+ ->setParentId(\Magento\Catalog\Model\Category::TREE_ROOT_ID)
+ ->setPath('1/2/334')
+ ->setLevel(2)
+ ->setAvailableSortBy('name')
+ ->setDefaultSortBy('name')
+ ->setIsActive(true)
+ ->setPosition(1)
+ ->setAvailableSortBy(['position'])
+ ->save();
+
+$rootCategoryForTestStoreGroup->setStoreId($store->load('test')->getId())
+ ->setName('category-test-store')
+ ->setUrlKey('category-test-store')
+ ->save();
+
+$storeCode = 'test';
+/** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */
+$storeRepository = $objectManager->create(\Magento\Store\Api\StoreRepositoryInterface::class);
+/** @var \Magento\Store\Api\Data\StoreInterface $store */
+$store = $storeRepository->get($storeCode);
+
+/** @var \Magento\Store\Model\Group $storeGroup */
+$storeGroup = $objectManager->create(\Magento\Store\Model\Group::class)
+ ->setWebsiteId('1')
+ ->setCode('test_store_group')
+ ->setName('Test Store Group')
+ ->setRootCategoryId($rootCategoryForTestStoreGroup->getId())
+ ->setDefaultStoreId($store->getId())
+ ->save();
+
+$store->setGroupId($storeGroup->getId())->save();
+
+/* Refresh stores memory cache */
+$objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->reinitStores();
diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/two_categories_per_two_store_groups_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/two_categories_per_two_store_groups_rollback.php
new file mode 100644
index 0000000000000..9592e9d0e69b4
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/two_categories_per_two_store_groups_rollback.php
@@ -0,0 +1,47 @@
+get(Registry::class);
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+// Delete first category
+/** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class);
+$searchCriteria = $searchCriteriaBuilder->addFilter('name', 'Category 1')->create();
+/** @var CategoryListInterface $categoryList */
+$categoryList = $objectManager->get(CategoryListInterface::class);
+$categories = $categoryList->getList($searchCriteria)->getItems();
+/** @var CategoryRepositoryInterface $categoryRepository */
+$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class);
+foreach ($categories as $category) {
+ $categoryRepository->delete($category);
+}
+// Delete second category
+$searchCriteria = $searchCriteriaBuilder->addFilter('name', 'Category 2')->create();
+$categories = $categoryList->getList($searchCriteria)->getItems();
+foreach ($categories as $category) {
+ $categoryRepository->delete($category);
+}
+// Delete store group
+/** @var Group $store */
+$storeGroup = $objectManager->get(Group::class);
+$storeGroup->load('test_store_group', 'code');
+if ($storeGroup->getId()) {
+ $storeGroup->delete();
+}
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes.php b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes.php
index 95f69b1e522ee..515cda55ec584 100644
--- a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes.php
+++ b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes.php
@@ -38,6 +38,15 @@
\Magento\Tax\Model\ClassModel::TAX_CLASS_TYPE_PRODUCT
)->save();
+// Tax class created but not used in the rule to ensure that unused tax classes are handled properly
+$productTaxClass3 = $objectManager->create(
+ \Magento\Tax\Model\ClassModel::class
+)->setClassName(
+ 'ProductTaxClass3'
+)->setClassType(
+ \Magento\Tax\Model\ClassModel::TAX_CLASS_TYPE_PRODUCT
+)->save();
+
$taxRate = [
'tax_country_id' => 'US',
'tax_region_id' => '12',
diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes_rollback.php b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes_rollback.php
new file mode 100644
index 0000000000000..51ba48b881736
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes_rollback.php
@@ -0,0 +1,43 @@
+get(\Magento\Tax\Model\ResourceModel\Calculation\Rule::class);
+foreach ($taxRules as $taxRuleCode) {
+ $taxRule = $objectManager->create(\Magento\Tax\Model\Calculation\Rule::class);
+ $taxRuleResource->load($taxRule, $taxRuleCode, 'code');
+ $taxRuleResource->delete($taxRule);
+}
+
+/** @var \Magento\Tax\Model\ResourceModel\TaxClass $resourceModel */
+$resourceModel = $objectManager->get(\Magento\Tax\Model\ResourceModel\TaxClass::class);
+
+foreach ($taxClasses as $taxClass) {
+ try {
+ /** @var \Magento\Tax\Model\ClassModel $taxClassEntity */
+ $taxClassEntity = $objectManager->create(\Magento\Tax\Model\ClassModel::class);
+ $resourceModel->load($taxClassEntity, $taxClass, 'class_name');
+ $resourceModel->delete($taxClassEntity);
+ } catch (\Magento\Framework\Exception\CouldNotDeleteException $couldNotDeleteException) {
+ // It's okay if the entity already wiped from the database
+ }
+}
diff --git a/lib/internal/Magento/Framework/Exception/AbstractAggregateException.php b/lib/internal/Magento/Framework/Exception/AbstractAggregateException.php
index 92a850511295a..ff142c5319006 100644
--- a/lib/internal/Magento/Framework/Exception/AbstractAggregateException.php
+++ b/lib/internal/Magento/Framework/Exception/AbstractAggregateException.php
@@ -78,6 +78,10 @@ public function addError(Phrase $phrase)
return $this;
}
+ /**
+ * @param LocalizedException $exception
+ * @return $this
+ */
public function addException(LocalizedException $exception)
{
$this->addErrorCalls++;
diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/Filter/Operator.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/Filter/Operator.php
index acd656e70ec62..ae2b8bfeabd1b 100644
--- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/Filter/Operator.php
+++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/Filter/Operator.php
@@ -65,7 +65,7 @@ public static function getOperators() : array
return $type->getConstants();
}
- /*
+ /**
* Convert operator to string
*
* @return string
diff --git a/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessor/SimpleConstructor.php b/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessor/SimpleConstructor.php
index f457a96e22a37..f875861e42b02 100644
--- a/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessor/SimpleConstructor.php
+++ b/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessor/SimpleConstructor.php
@@ -19,6 +19,10 @@ class SimpleConstructor
*/
private $name;
+ /**
+ * @param int $entityId
+ * @param string $name
+ */
public function __construct(
int $entityId,
string $name
diff --git a/lib/web/mage/dropdowns.js b/lib/web/mage/dropdowns.js
index 6c1389924a24b..1496a1c65d957 100644
--- a/lib/web/mage/dropdowns.js
+++ b/lib/web/mage/dropdowns.js
@@ -127,11 +127,9 @@ define([
}
elem.on('click.toggleDropdown', function () {
- var el;
+ var el = actionElem;
if (options.autoclose === true) {
- el = actionElem;
-
actionElem = $();
$(document).trigger('click.hideDropdown');
actionElem = el;
diff --git a/setup/src/Magento/Setup/Console/Command/GenerateFixturesCommand.php b/setup/src/Magento/Setup/Console/Command/GenerateFixturesCommand.php
index e084cd8ff6cad..03b74f637a7da 100644
--- a/setup/src/Magento/Setup/Console/Command/GenerateFixturesCommand.php
+++ b/setup/src/Magento/Setup/Console/Command/GenerateFixturesCommand.php
@@ -157,6 +157,10 @@ private function clearChangelog()
}
}
+ /**
+ * @param \Magento\Setup\Fixtures\Fixture $fixture
+ * @param OutputInterface $output
+ */
private function executeFixture(\Magento\Setup\Fixtures\Fixture $fixture, OutputInterface $output)
{
$output->write('' . $fixture->getActionTitle() . '... ');
diff --git a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php
index c3f292ce76e1e..cd2d4a77db65a 100755
--- a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php
+++ b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php
@@ -518,6 +518,10 @@ private function autoPrependText()
}
}
+ /**
+ * @param array $messages
+ * @return array
+ */
private function reduceBuffer($messages)
{
// We need to know if the two last chars are PHP_EOL