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