diff --git a/vendor/magento/module-sales/Model/ResourceModel/Report/Bestsellers.php b/vendor/magento/module-sales/Model/ResourceModel/Report/Bestsellers.php
index e37e8ab843e73..13a8f8d9589f3 100644
--- a/vendor/magento/module-sales/Model/ResourceModel/Report/Bestsellers.php
+++ b/vendor/magento/module-sales/Model/ResourceModel/Report/Bestsellers.php
@@ -5,25 +5,39 @@
*/
namespace Magento\Sales\Model\ResourceModel\Report;
+use Magento\Catalog\Model\ResourceModel\Product;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\DB\Select;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Model\ResourceModel\Db\Context;
+use Magento\Framework\Stdlib\DateTime\DateTime;
+use Magento\Framework\Stdlib\DateTime\Timezone\Validator;
+use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
+use Magento\Reports\Model\FlagFactory;
+use Magento\Sales\Model\ResourceModel\Helper;
+use Magento\Store\Model\Store;
+use Magento\Store\Model\StoreManagerInterface;
+use Psr\Log\LoggerInterface;
+
/**
* Bestsellers report resource model
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Bestsellers extends AbstractReport
{
- const AGGREGATION_DAILY = 'daily';
+ public const AGGREGATION_DAILY = 'daily';
- const AGGREGATION_MONTHLY = 'monthly';
+ public const AGGREGATION_MONTHLY = 'monthly';
- const AGGREGATION_YEARLY = 'yearly';
+ public const AGGREGATION_YEARLY = 'yearly';
/**
- * @var \Magento\Catalog\Model\ResourceModel\Product
+ * @var Product
*/
protected $_productResource;
/**
- * @var \Magento\Sales\Model\ResourceModel\Helper
+ * @var Helper
*/
protected $_salesResourceHelper;
@@ -37,29 +51,41 @@ class Bestsellers extends AbstractReport
];
/**
- * @param \Magento\Framework\Model\ResourceModel\Db\Context $context
- * @param \Psr\Log\LoggerInterface $logger
- * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
- * @param \Magento\Reports\Model\FlagFactory $reportsFlagFactory
- * @param \Magento\Framework\Stdlib\DateTime\Timezone\Validator $timezoneValidator
- * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime
- * @param \Magento\Catalog\Model\ResourceModel\Product $productResource
- * @param \Magento\Sales\Model\ResourceModel\Helper $salesResourceHelper
+ * @var StoreManagerInterface
+ */
+ protected $storeManager;
+
+ /**
+ * @var array
+ */
+ private array $rangesByQuery = [];
+
+ /**
+ * @param Context $context
+ * @param LoggerInterface $logger
+ * @param TimezoneInterface $localeDate
+ * @param FlagFactory $reportsFlagFactory
+ * @param Validator $timezoneValidator
+ * @param DateTime $dateTime
+ * @param Product $productResource
+ * @param Helper $salesResourceHelper
+ * @param string|null $connectionName
* @param array $ignoredProductTypes
- * @param string $connectionName
+ * @param StoreManagerInterface|null $storeManager
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
- \Magento\Framework\Model\ResourceModel\Db\Context $context,
- \Psr\Log\LoggerInterface $logger,
- \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
- \Magento\Reports\Model\FlagFactory $reportsFlagFactory,
- \Magento\Framework\Stdlib\DateTime\Timezone\Validator $timezoneValidator,
- \Magento\Framework\Stdlib\DateTime\DateTime $dateTime,
- \Magento\Catalog\Model\ResourceModel\Product $productResource,
- \Magento\Sales\Model\ResourceModel\Helper $salesResourceHelper,
- $connectionName = null,
- array $ignoredProductTypes = []
+ Context $context,
+ LoggerInterface $logger,
+ TimezoneInterface $localeDate,
+ FlagFactory $reportsFlagFactory,
+ Validator $timezoneValidator,
+ DateTime $dateTime,
+ Product $productResource,
+ Helper $salesResourceHelper,
+ ?string $connectionName = null,
+ array $ignoredProductTypes = [],
+ ?StoreManagerInterface $storeManager = null
) {
parent::__construct(
$context,
@@ -73,6 +99,7 @@ public function __construct(
$this->_productResource = $productResource;
$this->_salesResourceHelper = $salesResourceHelper;
$this->ignoredProductTypes = array_merge($this->ignoredProductTypes, $ignoredProductTypes);
+ $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
}
/**
@@ -92,116 +119,217 @@ protected function _construct()
* @param string|int|\DateTime|array|null $to
* @return $this
* @throws \Exception
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function aggregate($from = null, $to = null)
{
$connection = $this->getConnection();
- //$this->getConnection()->beginTransaction();
-
- try {
- if ($from !== null || $to !== null) {
- $subSelect = $this->_getTableDateRangeSelect(
- $this->getTable('sales_order'),
- 'created_at',
- 'updated_at',
- $from,
- $to
- );
- } else {
- $subSelect = null;
+ $this->clearByDateRange($from, $to);
+ foreach ($this->storeManager->getStores(true) as $store) {
+ $this->processStoreAggregate($store->getId(), $from, $to);
+ }
+
+ $columns = [
+ 'period' => 'period',
+ 'store_id' => new \Zend_Db_Expr(Store::DEFAULT_STORE_ID),
+ 'product_id' => 'product_id',
+ 'product_name' => new \Zend_Db_Expr('MIN(product_name)'),
+ 'product_price' => new \Zend_Db_Expr('MIN(product_price)'),
+ 'qty_ordered' => new \Zend_Db_Expr('SUM(qty_ordered)'),
+ ];
+
+ $select = $connection->select();
+ $select->reset();
+ $select->from(
+ $this->getMainTable(),
+ $columns
+ )->where(
+ 'store_id <> ?',
+ Store::DEFAULT_STORE_ID
+ );
+ $subSelect = $this->getRangeSubSelect($from, $to);
+ if ($subSelect !== null) {
+ $select->where($this->_makeConditionFromDateRangeSelect($subSelect, 'period'));
+ }
+
+ $select->group(['period', 'product_id']);
+ $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns));
+ $connection->query($insertQuery);
+
+ $this->_updateRatingPos(self::AGGREGATION_DAILY);
+ $this->_updateRatingPos(self::AGGREGATION_MONTHLY);
+ $this->_updateRatingPos(self::AGGREGATION_YEARLY);
+ $this->_setFlagData(\Magento\Reports\Model\Flag::REPORT_BESTSELLERS_FLAG_CODE);
+
+ return $this;
+ }
+
+ /**
+ * Clear aggregate existing data by range
+ *
+ * @param string|int|\DateTime|array|null $from
+ * @param string|int|\DateTime|array|null $to
+ * @return void
+ * @throws LocalizedException
+ */
+ private function clearByDateRange($from = null, $to = null): void
+ {
+ $subSelect = $this->getRangeSubSelect($from, $to);
+ $this->clearTableRanges($this->getMainTable(), $from, $to, $subSelect);
+ }
+
+ /**
+ * Clear table by date range
+ *
+ * @param string $table
+ * @param ?string $from
+ * @param ?string $to
+ * @param null|Select|string $subSelect
+ * @return void
+ */
+ private function clearTableRanges($table, $from = null, $to = null, $subSelect = null): void
+ {
+ if ($from === null && $to === null) {
+ $this->_truncateTable($table);
+ return;
+ }
+
+ if ($subSelect !== null) {
+ $dataRange = $this->getRange($subSelect);
+ $deleteCondition = $this->getConnection()->prepareSqlCondition('period', ['in' => $dataRange]);
+ $this->getConnection()->delete($table, $deleteCondition);
+ return;
+ } else {
+ $condition = [];
+ if ($from !== null) {
+ $condition[] = $this->getConnection()->quoteInto('period >= ?', $from);
}
- $this->_clearTableByDateRange($this->getMainTable(), $from, $to, $subSelect);
- // convert dates to current admin timezone
- $periodExpr = $connection->getDatePartSql(
- $this->getStoreTZOffsetQuery(
- ['source_table' => $this->getTable('sales_order')],
- 'source_table.created_at',
- $from,
- $to
- )
- );
- $select = $connection->select();
-
- $select->group([$periodExpr, 'source_table.store_id', 'order_item.product_id']);
-
- $columns = [
- 'period' => $periodExpr,
- 'store_id' => 'source_table.store_id',
- 'product_id' => 'order_item.product_id',
- 'product_name' => new \Zend_Db_Expr('MIN(order_item.name)'),
- 'product_price' => new \Zend_Db_Expr(
- 'MIN(IF(order_item_parent.base_price, order_item_parent.base_price, order_item.base_price))' .
- '* MIN(source_table.base_to_global_rate)'
- ),
- 'qty_ordered' => new \Zend_Db_Expr('SUM(order_item.qty_ordered)'),
- ];
-
- $select->from(
- ['source_table' => $this->getTable('sales_order')],
- $columns
- )->joinInner(
- ['order_item' => $this->getTable('sales_order_item')],
- 'order_item.order_id = source_table.entity_id',
- []
- )->joinLeft(
- ['order_item_parent' => $this->getTable('sales_order_item')],
- 'order_item.parent_item_id = order_item_parent.item_id',
- []
- )->where(
- 'source_table.state != ?',
- \Magento\Sales\Model\Order::STATE_CANCELED
- )->where(
- 'order_item.product_type NOT IN(?)',
- $this->ignoredProductTypes
- );
+ if ($to !== null) {
+ $condition[] = $this->getConnection()->quoteInto('period <= ?', $to);
+ }
+ $deleteCondition = implode(' AND ', $condition);
+ }
+ $this->getConnection()->delete($table, $deleteCondition);
+ }
+
+ /**
+ * Get dates range to clear the table
+ *
+ * @param Select $select
+ * @return array
+ */
+ private function getRange(Select $select): array
+ {
+ $queryHash = sha1($select->__toString());
+ if (!isset($this->rangesByQuery[$queryHash])) {
- if ($subSelect !== null) {
- $select->having($this->_makeConditionFromDateRangeSelect($subSelect, 'period'));
+ $connection = $this->getConnection();
+ try {
+ $query = $connection->query($select);
+ $range = $query->fetchAll(\Zend_Db::FETCH_COLUMN);
+ } catch (\Exception) {
+ $range = [];
}
- $select->useStraightJoin();
- // important!
- $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns));
- $connection->query($insertQuery);
-
- $columns = [
- 'period' => 'period',
- 'store_id' => new \Zend_Db_Expr(\Magento\Store\Model\Store::DEFAULT_STORE_ID),
- 'product_id' => 'product_id',
- 'product_name' => new \Zend_Db_Expr('MIN(product_name)'),
- 'product_price' => new \Zend_Db_Expr('MIN(product_price)'),
- 'qty_ordered' => new \Zend_Db_Expr('SUM(qty_ordered)'),
- ];
-
- $select->reset();
- $select->from(
- $this->getMainTable(),
- $columns
- )->where(
- 'store_id <> ?',
- \Magento\Store\Model\Store::DEFAULT_STORE_ID
+ $this->rangesByQuery[$queryHash] = $range;
+ }
+ return $this->rangesByQuery[$queryHash];
+ }
+
+ /**
+ * Get report range sub-select
+ *
+ * @param string|int|\DateTime|array|null $from
+ * @param string|int|\DateTime|array|null $to
+ * @return Select|null
+ */
+ private function getRangeSubSelect($from = null, $to = null): ?Select
+ {
+ $subSelect = null;
+ if ($from !== null || $to !== null) {
+ $subSelect = $this->_getTableDateRangeSelect(
+ $this->getTable('sales_order'),
+ 'created_at',
+ 'updated_at',
+ $from,
+ $to
);
+ }
- if ($subSelect !== null) {
- $select->where($this->_makeConditionFromDateRangeSelect($subSelect, 'period'));
- }
+ return $subSelect;
+ }
+
+ /**
+ * Calculate report aggregate per store
+ *
+ * @param int|null $storeId
+ * @param string|int|\DateTime|array|null $from
+ * @param string|int|\DateTime|array|null $to
+ * @return void
+ * @throws LocalizedException
+ */
+ private function processStoreAggregate(?int $storeId, $from = null, $to = null): void
+ {
+ $connection = $this->getConnection();
- $select->group(['period', 'product_id']);
- $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns));
- $connection->query($insertQuery);
-
- // update rating
- $this->_updateRatingPos(self::AGGREGATION_DAILY);
- $this->_updateRatingPos(self::AGGREGATION_MONTHLY);
- $this->_updateRatingPos(self::AGGREGATION_YEARLY);
- $this->_setFlagData(\Magento\Reports\Model\Flag::REPORT_BESTSELLERS_FLAG_CODE);
- } catch (\Exception $e) {
- throw $e;
+ // convert dates to current admin timezone
+ $periodExpr = $connection->getDatePartSql(
+ $this->getStoreTZOffsetQuery(
+ ['source_table' => $this->getTable('sales_order')],
+ 'source_table.created_at',
+ $from,
+ $to
+ )
+ );
+
+ $subSelect = $this->getRangeSubSelect($from, $to);
+ if ($subSelect) {
+ $dataRange = $this->getRange($subSelect);
+ $whereCondition = $connection->prepareSqlCondition($periodExpr, ['in' => $dataRange]);
}
- return $this;
+ $select = $connection->select();
+ $select->group([$periodExpr, 'source_table.store_id', 'order_item.product_id']);
+
+ $columns = [
+ 'period' => $periodExpr,
+ 'store_id' => 'source_table.store_id',
+ 'product_id' => 'order_item.product_id',
+ 'product_name' => new \Zend_Db_Expr('MIN(order_item.name)'),
+ 'product_price' => new \Zend_Db_Expr(
+ 'MIN(IF(order_item_parent.base_price, order_item_parent.base_price, order_item.base_price))' .
+ '* MIN(source_table.base_to_global_rate)'
+ ),
+ 'qty_ordered' => new \Zend_Db_Expr('SUM(order_item.qty_ordered)'),
+ ];
+
+ $select->from(
+ ['source_table' => $this->getTable('sales_order')],
+ $columns
+ )->joinInner(
+ ['order_item' => $this->getTable('sales_order_item')],
+ 'order_item.order_id = source_table.entity_id',
+ []
+ )->joinLeft(
+ ['order_item_parent' => $this->getTable('sales_order_item')],
+ 'order_item.parent_item_id = order_item_parent.item_id',
+ []
+ )->where(
+ "source_table.entity_id IN (SELECT entity_id FROM " . $this->getTable('sales_order') .
+ " WHERE store_id = " . $storeId .
+ " AND state != '" . \Magento\Sales\Model\Order::STATE_CANCELED . "'" .
+ ($subSelect !== null ?
+ " AND " . $whereCondition :
+ '') . ")"
+ )->where(
+ 'order_item.product_type NOT IN(?)',
+ $this->ignoredProductTypes
+ );
+
+ $select->useStraightJoin();
+ // important!
+ $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns));
+ $connection->query($insertQuery);
}
/**
@@ -209,6 +337,7 @@ public function aggregate($from = null, $to = null)
*
* @param string $aggregation
* @return $this
+ * @throws LocalizedException
*/
protected function _updateRatingPos($aggregation)
{
diff --git a/vendor/magento/module-sales/etc/db_schema.xml b/vendor/magento/module-sales/etc/db_schema.xml
index 112e927bf4c9d..3297d96c6ef7d 100644
--- a/vendor/magento/module-sales/etc/db_schema.xml
+++ b/vendor/magento/module-sales/etc/db_schema.xml
@@ -294,6 +294,11 @@