Collection.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Reports\Model\ResourceModel\Order;
  7. use Magento\Framework\DB\Select;
  8. /**
  9. * Reports orders collection
  10. *
  11. * @author Magento Core Team <core@magentocommerce.com>
  12. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  13. * @api
  14. * @since 100.0.2
  15. */
  16. class Collection extends \Magento\Sales\Model\ResourceModel\Order\Collection
  17. {
  18. /**
  19. * Is live
  20. *
  21. * @var bool
  22. */
  23. protected $_isLive = false;
  24. /**
  25. * Sales amount expression
  26. *
  27. * @var string
  28. */
  29. protected $_salesAmountExpression;
  30. /**
  31. * Core store config
  32. *
  33. * @var \Magento\Framework\App\Config\ScopeConfigInterface
  34. */
  35. protected $_scopeConfig;
  36. /**
  37. * Store manager instance
  38. *
  39. * @var \Magento\Store\Model\StoreManagerInterface
  40. */
  41. protected $_storeManager;
  42. /**
  43. * Locale date instance
  44. *
  45. * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface
  46. */
  47. protected $_localeDate;
  48. /**
  49. * Order config instance
  50. *
  51. * @var \Magento\Sales\Model\Order\Config
  52. */
  53. protected $_orderConfig;
  54. /**
  55. * Reports order factory
  56. *
  57. * @var \Magento\Sales\Model\ResourceModel\Report\OrderFactory
  58. */
  59. protected $_reportOrderFactory;
  60. /**
  61. * Constructor
  62. *
  63. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
  64. * @param \Psr\Log\LoggerInterface $logger
  65. * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
  66. * @param \Magento\Framework\Event\ManagerInterface $eventManager
  67. * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot
  68. * @param \Magento\Framework\DB\Helper $coreResourceHelper
  69. * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
  70. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  71. * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
  72. * @param \Magento\Sales\Model\Order\Config $orderConfig
  73. * @param \Magento\Sales\Model\ResourceModel\Report\OrderFactory $reportOrderFactory
  74. * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
  75. * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource
  76. *
  77. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  78. */
  79. public function __construct(
  80. \Magento\Framework\Data\Collection\EntityFactory $entityFactory,
  81. \Psr\Log\LoggerInterface $logger,
  82. \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
  83. \Magento\Framework\Event\ManagerInterface $eventManager,
  84. \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot,
  85. \Magento\Framework\DB\Helper $coreResourceHelper,
  86. \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
  87. \Magento\Store\Model\StoreManagerInterface $storeManager,
  88. \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
  89. \Magento\Sales\Model\Order\Config $orderConfig,
  90. \Magento\Sales\Model\ResourceModel\Report\OrderFactory $reportOrderFactory,
  91. \Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
  92. \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null
  93. ) {
  94. parent::__construct(
  95. $entityFactory,
  96. $logger,
  97. $fetchStrategy,
  98. $eventManager,
  99. $entitySnapshot,
  100. $coreResourceHelper,
  101. $connection,
  102. $resource
  103. );
  104. $this->_scopeConfig = $scopeConfig;
  105. $this->_storeManager = $storeManager;
  106. $this->_localeDate = $localeDate;
  107. $this->_orderConfig = $orderConfig;
  108. $this->_reportOrderFactory = $reportOrderFactory;
  109. }
  110. /**
  111. * Check range for live mode
  112. *
  113. * @param string $range
  114. * @return $this
  115. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  116. */
  117. public function checkIsLive($range)
  118. {
  119. $this->_isLive = (bool)(!$this->_scopeConfig->getValue(
  120. 'sales/dashboard/use_aggregated_data',
  121. \Magento\Store\Model\ScopeInterface::SCOPE_STORE
  122. ));
  123. return $this;
  124. }
  125. /**
  126. * Retrieve is live flag for rep
  127. *
  128. * @return bool
  129. * @codeCoverageIgnore
  130. */
  131. public function isLive()
  132. {
  133. return $this->_isLive;
  134. }
  135. /**
  136. * Prepare report summary
  137. *
  138. * @param string $range
  139. * @param mixed $customStart
  140. * @param mixed $customEnd
  141. * @param int $isFilter
  142. * @return $this
  143. */
  144. public function prepareSummary($range, $customStart, $customEnd, $isFilter = 0)
  145. {
  146. $this->checkIsLive($range);
  147. if ($this->_isLive) {
  148. $this->_prepareSummaryLive($range, $customStart, $customEnd, $isFilter);
  149. } else {
  150. $this->_prepareSummaryAggregated($range, $customStart, $customEnd, $isFilter);
  151. }
  152. return $this;
  153. }
  154. /**
  155. * Get sales amount expression
  156. *
  157. * @return string
  158. */
  159. protected function _getSalesAmountExpression()
  160. {
  161. if (null === $this->_salesAmountExpression) {
  162. $connection = $this->getConnection();
  163. $expressionTransferObject = new \Magento\Framework\DataObject(
  164. [
  165. 'expression' => '%s - %s - %s - (%s - %s - %s)',
  166. 'arguments' => [
  167. $connection->getIfNullSql('main_table.base_total_invoiced', 0),
  168. $connection->getIfNullSql('main_table.base_tax_invoiced', 0),
  169. $connection->getIfNullSql('main_table.base_shipping_invoiced', 0),
  170. $connection->getIfNullSql('main_table.base_total_refunded', 0),
  171. $connection->getIfNullSql('main_table.base_tax_refunded', 0),
  172. $connection->getIfNullSql('main_table.base_shipping_refunded', 0),
  173. ],
  174. ]
  175. );
  176. $this->_eventManager->dispatch(
  177. 'sales_prepare_amount_expression',
  178. ['collection' => $this, 'expression_object' => $expressionTransferObject]
  179. );
  180. $this->_salesAmountExpression = vsprintf(
  181. $expressionTransferObject->getExpression(),
  182. $expressionTransferObject->getArguments()
  183. );
  184. }
  185. return $this->_salesAmountExpression;
  186. }
  187. /**
  188. * Prepare report summary from live data
  189. *
  190. * @param string $range
  191. * @param mixed $customStart
  192. * @param mixed $customEnd
  193. * @param int $isFilter
  194. * @return $this
  195. */
  196. protected function _prepareSummaryLive($range, $customStart, $customEnd, $isFilter = 0)
  197. {
  198. $this->setMainTable('sales_order');
  199. $connection = $this->getConnection();
  200. /**
  201. * Reset all columns, because result will group only by 'created_at' field
  202. */
  203. $this->getSelect()->reset(\Magento\Framework\DB\Select::COLUMNS);
  204. $expression = $this->_getSalesAmountExpression();
  205. if ($isFilter == 0) {
  206. $this->getSelect()->columns(
  207. [
  208. 'revenue' => new \Zend_Db_Expr(
  209. sprintf(
  210. 'SUM((%s) * %s)',
  211. $expression,
  212. $connection->getIfNullSql('main_table.base_to_global_rate', 0)
  213. )
  214. ),
  215. ]
  216. );
  217. } else {
  218. $this->getSelect()->columns(['revenue' => new \Zend_Db_Expr(sprintf('SUM(%s)', $expression))]);
  219. }
  220. $dateRange = $this->getDateRange($range, $customStart, $customEnd);
  221. $tzRangeOffsetExpression = $this->_getTZRangeOffsetExpression(
  222. $range,
  223. 'created_at',
  224. $dateRange['from'],
  225. $dateRange['to']
  226. );
  227. $this->getSelect()->columns(
  228. ['quantity' => 'COUNT(main_table.entity_id)', 'range' => $tzRangeOffsetExpression]
  229. )->where(
  230. 'main_table.state NOT IN (?)',
  231. [\Magento\Sales\Model\Order::STATE_PENDING_PAYMENT, \Magento\Sales\Model\Order::STATE_NEW]
  232. )->order(
  233. 'range',
  234. \Magento\Framework\DB\Select::SQL_ASC
  235. )->group(
  236. $tzRangeOffsetExpression
  237. );
  238. $this->addFieldToFilter('created_at', $dateRange);
  239. return $this;
  240. }
  241. /**
  242. * Prepare report summary from aggregated data
  243. *
  244. * @param string $range
  245. * @param string|null $customStart
  246. * @param string|null $customEnd
  247. * @return $this
  248. */
  249. protected function _prepareSummaryAggregated($range, $customStart, $customEnd)
  250. {
  251. $this->setMainTable('sales_order_aggregated_created');
  252. /**
  253. * Reset all columns, because result will group only by 'created_at' field
  254. */
  255. $this->getSelect()->reset(\Magento\Framework\DB\Select::COLUMNS);
  256. $rangePeriod = $this->_getRangeExpressionForAttribute($range, 'main_table.period');
  257. $tableName = $this->getConnection()->quoteIdentifier('main_table.period');
  258. $rangePeriodAggregateStmt = str_replace($tableName, "MIN({$tableName})", $rangePeriod);
  259. $this->getSelect()->columns(
  260. [
  261. 'revenue' => 'SUM(main_table.total_revenue_amount)',
  262. 'quantity' => 'SUM(main_table.orders_count)',
  263. 'range' => $rangePeriodAggregateStmt,
  264. ]
  265. )->order(
  266. 'range'
  267. )->group(
  268. $rangePeriod
  269. );
  270. $this->getSelect()->where(
  271. $this->_getConditionSql('main_table.period', $this->getDateRange($range, $customStart, $customEnd))
  272. );
  273. $statuses = $this->_orderConfig->getStateStatuses(\Magento\Sales\Model\Order::STATE_CANCELED);
  274. if (empty($statuses)) {
  275. $statuses = [0];
  276. }
  277. $this->addFieldToFilter('main_table.order_status', ['nin' => $statuses]);
  278. return $this;
  279. }
  280. /**
  281. * Get range expression
  282. *
  283. * @param string $range
  284. * @return \Zend_Db_Expr
  285. */
  286. protected function _getRangeExpression($range)
  287. {
  288. switch ($range) {
  289. case '24h':
  290. $expression = $this->getConnection()->getConcatSql(
  291. [
  292. $this->getConnection()->getDateFormatSql('{{attribute}}', '%Y-%m-%d %H:'),
  293. $this->getConnection()->quote('00'),
  294. ]
  295. );
  296. break;
  297. case '7d':
  298. case '1m':
  299. $expression = $this->getConnection()->getDateFormatSql('{{attribute}}', '%Y-%m-%d');
  300. break;
  301. case '1y':
  302. case '2y':
  303. case 'custom':
  304. default:
  305. $expression = $this->getConnection()->getDateFormatSql('{{attribute}}', '%Y-%m');
  306. break;
  307. }
  308. return $expression;
  309. }
  310. /**
  311. * Retrieve range expression adapted for attribute
  312. *
  313. * @param string $range
  314. * @param string $attribute
  315. * @return string
  316. */
  317. protected function _getRangeExpressionForAttribute($range, $attribute)
  318. {
  319. $expression = $this->_getRangeExpression($range);
  320. return str_replace('{{attribute}}', $this->getConnection()->quoteIdentifier($attribute), $expression);
  321. }
  322. /**
  323. * Retrieve query for attribute with timezone conversion
  324. *
  325. * @param string $range
  326. * @param string $attribute
  327. * @param string|null $from
  328. * @param string|null $to
  329. * @return string
  330. */
  331. protected function _getTZRangeOffsetExpression($range, $attribute, $from = null, $to = null)
  332. {
  333. return str_replace(
  334. '{{attribute}}',
  335. $this->_reportOrderFactory->create()->getStoreTZOffsetQuery($this->getMainTable(), $attribute, $from, $to),
  336. $this->_getRangeExpression($range)
  337. );
  338. }
  339. /**
  340. * Retrieve range expression with timezone conversion adapted for attribute
  341. *
  342. * @param string $range
  343. * @param string $attribute
  344. * @param string $tzFrom
  345. * @param string $tzTo
  346. * @return string
  347. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  348. */
  349. protected function _getTZRangeExpressionForAttribute($range, $attribute, $tzFrom = '+00:00', $tzTo = null)
  350. {
  351. if (null == $tzTo) {
  352. $tzTo = $this->_localeDate->scopeDate()->format('P');
  353. }
  354. $connection = $this->getConnection();
  355. $expression = $this->_getRangeExpression($range);
  356. $attribute = $connection->quoteIdentifier($attribute);
  357. $periodExpr = $connection->getDateAddSql(
  358. $attribute,
  359. $tzTo,
  360. \Magento\Framework\DB\Adapter\AdapterInterface::INTERVAL_HOUR
  361. );
  362. return str_replace('{{attribute}}', $periodExpr, $expression);
  363. }
  364. /**
  365. * Calculate From and To dates (or times) by given period
  366. *
  367. * @param string $range
  368. * @param string $customStart
  369. * @param string $customEnd
  370. * @param bool $returnObjects
  371. * @return array
  372. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  373. */
  374. public function getDateRange($range, $customStart, $customEnd, $returnObjects = false)
  375. {
  376. $dateEnd = new \DateTime();
  377. $dateStart = new \DateTime();
  378. // go to the end of a day
  379. $dateEnd->setTime(23, 59, 59);
  380. $dateStart->setTime(0, 0, 0);
  381. switch ($range) {
  382. case '24h':
  383. $dateEnd = new \DateTime();
  384. $dateEnd->modify('+1 hour');
  385. $dateStart = clone $dateEnd;
  386. $dateStart->modify('-1 day');
  387. break;
  388. case '7d':
  389. // substract 6 days we need to include
  390. // only today and not hte last one from range
  391. $dateStart->modify('-6 days');
  392. break;
  393. case '1m':
  394. $dateStart->setDate(
  395. $dateStart->format('Y'),
  396. $dateStart->format('m'),
  397. $this->_scopeConfig->getValue(
  398. 'reports/dashboard/mtd_start',
  399. \Magento\Store\Model\ScopeInterface::SCOPE_STORE
  400. )
  401. );
  402. break;
  403. case 'custom':
  404. $dateStart = $customStart ? $customStart : $dateEnd;
  405. $dateEnd = $customEnd ? $customEnd : $dateEnd;
  406. break;
  407. case '1y':
  408. case '2y':
  409. $startMonthDay = explode(
  410. ',',
  411. $this->_scopeConfig->getValue(
  412. 'reports/dashboard/ytd_start',
  413. \Magento\Store\Model\ScopeInterface::SCOPE_STORE
  414. )
  415. );
  416. $startMonth = isset($startMonthDay[0]) ? (int)$startMonthDay[0] : 1;
  417. $startDay = isset($startMonthDay[1]) ? (int)$startMonthDay[1] : 1;
  418. $dateStart->setDate($dateStart->format('Y'), $startMonth, $startDay);
  419. if ($range == '2y') {
  420. $dateStart->modify('-1 year');
  421. }
  422. break;
  423. }
  424. if ($returnObjects) {
  425. return [$dateStart, $dateEnd];
  426. } else {
  427. return ['from' => $dateStart, 'to' => $dateEnd, 'datetime' => true];
  428. }
  429. }
  430. /**
  431. * Add item count expression
  432. *
  433. * @return $this
  434. */
  435. public function addItemCountExpr()
  436. {
  437. $this->getSelect()->columns(['items_count' => 'total_item_count'], 'main_table');
  438. return $this;
  439. }
  440. /**
  441. * Calculate totals report
  442. *
  443. * @param int $isFilter
  444. * @return $this
  445. */
  446. public function calculateTotals($isFilter = 0)
  447. {
  448. if ($this->isLive()) {
  449. $this->_calculateTotalsLive($isFilter);
  450. } else {
  451. $this->_calculateTotalsAggregated($isFilter);
  452. }
  453. return $this;
  454. }
  455. /**
  456. * Calculate totals live report
  457. *
  458. * @param int $isFilter
  459. * @return $this
  460. */
  461. protected function _calculateTotalsLive($isFilter = 0)
  462. {
  463. $this->setMainTable('sales_order');
  464. $this->removeAllFieldsFromSelect();
  465. $connection = $this->getConnection();
  466. $baseTaxInvoiced = $connection->getIfNullSql('main_table.base_tax_invoiced', 0);
  467. $baseTaxRefunded = $connection->getIfNullSql('main_table.base_tax_refunded', 0);
  468. $baseShippingInvoiced = $connection->getIfNullSql('main_table.base_shipping_invoiced', 0);
  469. $baseShippingRefunded = $connection->getIfNullSql('main_table.base_shipping_refunded', 0);
  470. $revenueExp = $this->_getSalesAmountExpression();
  471. $taxExp = sprintf('%s - %s', $baseTaxInvoiced, $baseTaxRefunded);
  472. $shippingExp = sprintf('%s - %s', $baseShippingInvoiced, $baseShippingRefunded);
  473. if ($isFilter == 0) {
  474. $rateExp = $connection->getIfNullSql('main_table.base_to_global_rate', 0);
  475. $this->getSelect()->columns(
  476. [
  477. 'revenue' => new \Zend_Db_Expr(sprintf('SUM((%s) * %s)', $revenueExp, $rateExp)),
  478. 'tax' => new \Zend_Db_Expr(sprintf('SUM((%s) * %s)', $taxExp, $rateExp)),
  479. 'shipping' => new \Zend_Db_Expr(sprintf('SUM((%s) * %s)', $shippingExp, $rateExp)),
  480. ]
  481. );
  482. } else {
  483. $this->getSelect()->columns(
  484. [
  485. 'revenue' => new \Zend_Db_Expr(sprintf('SUM(%s)', $revenueExp)),
  486. 'tax' => new \Zend_Db_Expr(sprintf('SUM(%s)', $taxExp)),
  487. 'shipping' => new \Zend_Db_Expr(sprintf('SUM(%s)', $shippingExp)),
  488. ]
  489. );
  490. }
  491. $this->getSelect()->columns(
  492. ['quantity' => 'COUNT(main_table.entity_id)']
  493. )->where(
  494. 'main_table.state NOT IN (?)',
  495. [\Magento\Sales\Model\Order::STATE_PENDING_PAYMENT, \Magento\Sales\Model\Order::STATE_NEW]
  496. );
  497. return $this;
  498. }
  499. /**
  500. * Calculate totals aggregated report
  501. *
  502. * @param int $isFilter
  503. * @return $this
  504. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  505. */
  506. protected function _calculateTotalsAggregated($isFilter = 0)
  507. {
  508. $this->setMainTable('sales_order_aggregated_created');
  509. $this->removeAllFieldsFromSelect();
  510. $this->getSelect()->columns(
  511. [
  512. 'revenue' => 'SUM(main_table.total_revenue_amount)',
  513. 'tax' => 'SUM(main_table.total_tax_amount_actual)',
  514. 'shipping' => 'SUM(main_table.total_shipping_amount_actual)',
  515. 'quantity' => 'SUM(orders_count)',
  516. ]
  517. );
  518. $statuses = $this->_orderConfig->getStateStatuses(\Magento\Sales\Model\Order::STATE_CANCELED);
  519. if (empty($statuses)) {
  520. $statuses = [0];
  521. }
  522. $this->getSelect()->where('main_table.order_status NOT IN(?)', $statuses);
  523. return $this;
  524. }
  525. /**
  526. * Calculate lifitime sales
  527. *
  528. * @param int $isFilter
  529. * @return $this
  530. */
  531. public function calculateSales($isFilter = 0)
  532. {
  533. $statuses = $this->_orderConfig->getStateStatuses(\Magento\Sales\Model\Order::STATE_CANCELED);
  534. if (empty($statuses)) {
  535. $statuses = [0];
  536. }
  537. $connection = $this->getConnection();
  538. if ($this->_scopeConfig->getValue(
  539. 'sales/dashboard/use_aggregated_data',
  540. \Magento\Store\Model\ScopeInterface::SCOPE_STORE
  541. )
  542. ) {
  543. $this->setMainTable('sales_order_aggregated_created');
  544. $this->removeAllFieldsFromSelect();
  545. $averageExpr = $connection->getCheckSql(
  546. 'SUM(main_table.orders_count) > 0',
  547. 'SUM(main_table.total_revenue_amount)/SUM(main_table.orders_count)',
  548. 0
  549. );
  550. $this->getSelect()->columns(
  551. ['lifetime' => 'SUM(main_table.total_revenue_amount)', 'average' => $averageExpr]
  552. );
  553. if (!$isFilter) {
  554. $this->addFieldToFilter(
  555. 'store_id',
  556. ['eq' => $this->_storeManager->getStore(\Magento\Store\Model\Store::ADMIN_CODE)->getId()]
  557. );
  558. }
  559. $this->getSelect()->where('main_table.order_status NOT IN(?)', $statuses);
  560. } else {
  561. $this->setMainTable('sales_order');
  562. $this->removeAllFieldsFromSelect();
  563. $expr = $this->_getSalesAmountExpression();
  564. if ($isFilter == 0) {
  565. $expr = '(' . $expr . ') * main_table.base_to_global_rate';
  566. }
  567. $this->getSelect()->columns(
  568. ['lifetime' => "SUM({$expr})", 'average' => "AVG({$expr})"]
  569. )->where(
  570. 'main_table.status NOT IN(?)',
  571. $statuses
  572. )->where(
  573. 'main_table.state NOT IN(?)',
  574. [\Magento\Sales\Model\Order::STATE_NEW, \Magento\Sales\Model\Order::STATE_PENDING_PAYMENT]
  575. );
  576. }
  577. return $this;
  578. }
  579. /**
  580. * Set date range
  581. *
  582. * @param string $fromDate
  583. * @param string $toDate
  584. * @return $this
  585. */
  586. public function setDateRange($fromDate, $toDate)
  587. {
  588. $this->_reset()->addFieldToFilter(
  589. 'created_at',
  590. ['from' => $fromDate, 'to' => $toDate]
  591. )->addFieldToFilter(
  592. 'state',
  593. ['neq' => \Magento\Sales\Model\Order::STATE_CANCELED]
  594. )->getSelect()->columns(
  595. ['orders' => 'COUNT(DISTINCT(main_table.entity_id))']
  596. )->group(
  597. 'entity_id'
  598. );
  599. $this->getSelect()->columns(['items' => 'SUM(main_table.total_qty_ordered)']);
  600. return $this;
  601. }
  602. /**
  603. * Set store filter collection
  604. *
  605. * @param int[] $storeIds
  606. * @return $this
  607. */
  608. public function setStoreIds($storeIds)
  609. {
  610. $connection = $this->getConnection();
  611. $baseSubtotalInvoiced = $connection->getIfNullSql('main_table.base_subtotal_invoiced', 0);
  612. $baseDiscountRefunded = $connection->getIfNullSql('main_table.base_discount_refunded', 0);
  613. $baseSubtotalRefunded = $connection->getIfNullSql('main_table.base_subtotal_refunded', 0);
  614. $baseDiscountInvoiced = $connection->getIfNullSql('main_table.base_discount_invoiced', 0);
  615. $baseTotalInvocedCost = $connection->getIfNullSql('main_table.base_total_invoiced_cost', 0);
  616. if ($storeIds) {
  617. $this->getSelect()->columns(
  618. [
  619. 'subtotal' => 'SUM(main_table.base_subtotal)',
  620. 'tax' => 'SUM(main_table.base_tax_amount)',
  621. 'shipping' => 'SUM(main_table.base_shipping_amount)',
  622. 'discount' => 'SUM(main_table.base_discount_amount)',
  623. 'total' => 'SUM(main_table.base_grand_total)',
  624. 'invoiced' => 'SUM(main_table.base_total_paid)',
  625. 'refunded' => 'SUM(main_table.base_total_refunded)',
  626. 'profit' => "SUM({$baseSubtotalInvoiced}) " .
  627. "+ SUM({$baseDiscountRefunded}) - SUM({$baseSubtotalRefunded}) " .
  628. "- SUM({$baseDiscountInvoiced}) - SUM({$baseTotalInvocedCost})",
  629. ]
  630. );
  631. } else {
  632. $this->getSelect()->columns(
  633. [
  634. 'subtotal' => 'SUM(main_table.base_subtotal * main_table.base_to_global_rate)',
  635. 'tax' => 'SUM(main_table.base_tax_amount * main_table.base_to_global_rate)',
  636. 'shipping' => 'SUM(main_table.base_shipping_amount * main_table.base_to_global_rate)',
  637. 'discount' => 'SUM(main_table.base_discount_amount * main_table.base_to_global_rate)',
  638. 'total' => 'SUM(main_table.base_grand_total * main_table.base_to_global_rate)',
  639. 'invoiced' => 'SUM(main_table.base_total_paid * main_table.base_to_global_rate)',
  640. 'refunded' => 'SUM(main_table.base_total_refunded * main_table.base_to_global_rate)',
  641. 'profit' => "SUM({$baseSubtotalInvoiced} * main_table.base_to_global_rate) " .
  642. "+ SUM({$baseDiscountRefunded} * main_table.base_to_global_rate) " .
  643. "- SUM({$baseSubtotalRefunded} * main_table.base_to_global_rate) " .
  644. "- SUM({$baseDiscountInvoiced} * main_table.base_to_global_rate) " .
  645. "- SUM({$baseTotalInvocedCost} * main_table.base_to_global_rate)",
  646. ]
  647. );
  648. }
  649. return $this;
  650. }
  651. /**
  652. * Add group By customer attribute
  653. *
  654. * @return $this
  655. */
  656. public function groupByCustomer()
  657. {
  658. $this->getSelect()->where('main_table.customer_id IS NOT NULL')->group('main_table.customer_id');
  659. return $this;
  660. }
  661. /**
  662. * Join Customer Name (concat)
  663. *
  664. * @param string $alias
  665. * @return $this
  666. */
  667. public function joinCustomerName($alias = 'name')
  668. {
  669. $fields = ['main_table.customer_firstname', 'main_table.customer_lastname'];
  670. $fieldConcat = $this->getConnection()->getConcatSql($fields, ' ');
  671. $this->getSelect()->columns([$alias => $fieldConcat]);
  672. return $this;
  673. }
  674. /**
  675. * Add Order count field to select
  676. *
  677. * @return $this
  678. */
  679. public function addOrdersCount()
  680. {
  681. $this->addFieldToFilter('state', ['neq' => \Magento\Sales\Model\Order::STATE_CANCELED]);
  682. $this->getSelect()->columns(['orders_count' => 'COUNT(main_table.entity_id)']);
  683. return $this;
  684. }
  685. /**
  686. * Add revenue
  687. *
  688. * @param bool $convertCurrency
  689. * @return $this
  690. */
  691. public function addRevenueToSelect($convertCurrency = false)
  692. {
  693. $expr = $this->getTotalsExpression(
  694. !$convertCurrency,
  695. $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0),
  696. $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0),
  697. $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0)
  698. );
  699. $this->getSelect()->columns(['revenue' => $expr]);
  700. return $this;
  701. }
  702. /**
  703. * Add summary average totals
  704. *
  705. * @param int $storeId
  706. * @return $this
  707. */
  708. public function addSumAvgTotals($storeId = 0)
  709. {
  710. /**
  711. * calculate average and total amount
  712. */
  713. $expr = $this->getTotalsExpression(
  714. $storeId,
  715. $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0),
  716. $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0),
  717. $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0)
  718. );
  719. $this->getSelect()->columns(
  720. ['orders_avg_amount' => "AVG({$expr})"]
  721. )->columns(
  722. ['orders_sum_amount' => "SUM({$expr})"]
  723. );
  724. return $this;
  725. }
  726. /**
  727. * Get SQL expression for totals
  728. *
  729. * @param int $storeId
  730. * @param string $baseSubtotalRefunded
  731. * @param string $baseSubtotalCanceled
  732. * @param string $baseDiscountCanceled
  733. * @return string
  734. */
  735. protected function getTotalsExpression(
  736. $storeId,
  737. $baseSubtotalRefunded,
  738. $baseSubtotalCanceled,
  739. $baseDiscountCanceled
  740. ) {
  741. $template = ($storeId != 0)
  742. ? '(main_table.base_subtotal - %2$s - %1$s - ABS(main_table.base_discount_amount) - %3$s)'
  743. : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) + %3$s) '
  744. . ' * main_table.base_to_global_rate)';
  745. return sprintf($template, $baseSubtotalRefunded, $baseSubtotalCanceled, $baseDiscountCanceled);
  746. }
  747. /**
  748. * Sort order by total amount
  749. *
  750. * @param string $dir
  751. * @return $this
  752. */
  753. public function orderByTotalAmount($dir = self::SORT_ORDER_DESC)
  754. {
  755. $this->getSelect()->order('orders_sum_amount ' . $dir);
  756. return $this;
  757. }
  758. /**
  759. * Order by orders count
  760. *
  761. * @param string $dir
  762. * @return $this
  763. */
  764. public function orderByOrdersCount($dir = self::SORT_ORDER_DESC)
  765. {
  766. $this->getSelect()->order('orders_count ' . $dir);
  767. return $this;
  768. }
  769. /**
  770. * Order by customer registration
  771. *
  772. * @param string $dir
  773. * @return $this
  774. */
  775. public function orderByCustomerRegistration($dir = self::SORT_ORDER_DESC)
  776. {
  777. $this->setOrder('customer_id', $dir);
  778. return $this;
  779. }
  780. /**
  781. * Sort order by order created_at date
  782. *
  783. * @param string $dir
  784. * @return $this
  785. */
  786. public function orderByCreatedAt($dir = self::SORT_ORDER_DESC)
  787. {
  788. $this->setOrder('created_at', $dir);
  789. return $this;
  790. }
  791. /**
  792. * Get select count sql
  793. *
  794. * @return Select
  795. */
  796. public function getSelectCountSql()
  797. {
  798. $countSelect = clone $this->getSelect();
  799. $countSelect->reset(\Magento\Framework\DB\Select::ORDER);
  800. $countSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT);
  801. $countSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET);
  802. $countSelect->reset(\Magento\Framework\DB\Select::COLUMNS);
  803. $countSelect->reset(\Magento\Framework\DB\Select::GROUP);
  804. $countSelect->reset(\Magento\Framework\DB\Select::HAVING);
  805. $countSelect->columns("COUNT(DISTINCT main_table.entity_id)");
  806. return $countSelect;
  807. }
  808. /**
  809. * Initialize initial fields to select
  810. *
  811. * @return $this
  812. * @codeCoverageIgnore
  813. */
  814. protected function _initInitialFieldsToSelect()
  815. {
  816. // No fields should be initialized
  817. return $this;
  818. }
  819. /**
  820. * Add period filter by created_at attribute
  821. *
  822. * @param string $period
  823. * @return $this
  824. */
  825. public function addCreateAtPeriodFilter($period)
  826. {
  827. list($from, $to) = $this->getDateRange($period, 0, 0, true);
  828. $this->checkIsLive($period);
  829. if ($this->isLive()) {
  830. $fieldToFilter = 'created_at';
  831. } else {
  832. $fieldToFilter = 'period';
  833. }
  834. $this->addFieldToFilter(
  835. $fieldToFilter,
  836. [
  837. 'from' => $from->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT),
  838. 'to' => $to->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)
  839. ]
  840. );
  841. return $this;
  842. }
  843. }