diff --git a/src/OroCRM/Bundle/MagentoBundle/EventListener/CustomerDataGridListener.php b/src/OroCRM/Bundle/MagentoBundle/EventListener/CustomerDataGridListener.php index 198351ea9d8..a0bf620cd78 100644 --- a/src/OroCRM/Bundle/MagentoBundle/EventListener/CustomerDataGridListener.php +++ b/src/OroCRM/Bundle/MagentoBundle/EventListener/CustomerDataGridListener.php @@ -5,7 +5,6 @@ use Oro\Bundle\DataGridBundle\Datagrid\ParameterBag; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Event\PreBuild; -use Oro\Bundle\DataGridBundle\Extension\Sorter\OrmSorterExtension; use Oro\Bundle\FilterBundle\Grid\Extension\OrmFilterExtension; class CustomerDataGridListener @@ -18,7 +17,6 @@ public function onPreBuild(PreBuild $event) $config = $event->getConfig(); $parameters = $event->getParameters(); $this->addNewsletterSubscribers($config, $parameters); - $this->convertJoinsToSubQueries($config, $parameters); } /** @@ -79,158 +77,4 @@ protected function addNewsletterSubscribers(DatagridConfiguration $config, Param ]; $config->offsetSetByPath('[source][query]', $query); } - - /** - * @param DatagridConfiguration $config - * @param ParameterBag $parameters - */ - protected function convertJoinsToSubQueries(DatagridConfiguration $config, ParameterBag $parameters) - { - // by a performance reasons, convert some joins to sub-queries - $sorters = $parameters->get(OrmSorterExtension::SORTERS_ROOT_PARAM, []); - if (empty($sorters['channelName'])) { - $this->convertAssociationJoinToSubquery( - $config, - 'dataChannel', - 'channelName', - 'OroCRM\Bundle\ChannelBundle\Entity\Channel' - ); - } - if (empty($sorters['websiteName'])) { - $this->convertAssociationJoinToSubquery( - $config, - 'cw', - 'websiteName', - 'OroCRM\Bundle\MagentoBundle\Entity\Website' - ); - } - if (empty($sorters['customerGroup'])) { - $this->convertAssociationJoinToSubquery( - $config, - 'cg', - 'customerGroup', - 'OroCRM\Bundle\MagentoBundle\Entity\CustomerGroup' - ); - } - } - - /** - * @param DatagridConfiguration $config - * @param string $joinAlias - * @param string $columnAlias - * @param string $joinEntityClass - */ - private function convertAssociationJoinToSubquery( - DatagridConfiguration $config, - $joinAlias, - $columnAlias, - $joinEntityClass - ) { - list( - $join, - $joinPath, - $selectExpr, - $selectPath - ) = $this->findJoinAndSelectByAliases($config, $joinAlias, $columnAlias); - if (!$join || !$selectExpr) { - return; - } - - $subQuery = sprintf( - 'SELECT %1$s FROM %4$s AS %3$s WHERE %3$s = %2$s', - $selectExpr, - $join['join'], - $joinAlias, - $joinEntityClass - ); - if (!empty($join['condition'])) { - $subQuery .= sprintf(' AND %s', $join['condition']); - } - - $config->offsetSetByPath($selectPath, sprintf('(%s) AS %s', $subQuery, $columnAlias)); - $config->offsetUnsetByPath($joinPath); - } - - /** - * @param DatagridConfiguration $config - * @param string $joinAlias - * @param string $columnAlias - * - * @return array [join, join path, select expression without column alias, select item path] - */ - private function findJoinAndSelectByAliases(DatagridConfiguration $config, $joinAlias, $columnAlias) - { - list($join, $joinPath) = $this->findJoinByAlias($config, $joinAlias, '[source][query][join][left]'); - $selectExpr = null; - $selectPath = null; - if (null !== $join) { - list($selectExpr, $selectPath) = $this->findSelectExprByAlias($config, $columnAlias); - } - - return [$join, $joinPath, $selectExpr, $selectPath]; - } - - /** - * @param DatagridConfiguration $config - * @param string $joinAlias - * @param string $joinsPath - * - * @return array [join, join path] - */ - private function findJoinByAlias(DatagridConfiguration $config, $joinAlias, $joinsPath) - { - $foundJoin = null; - $foundJoinPath = null; - $joins = $config->offsetGetByPath($joinsPath, []); - foreach ($joins as $key => $join) { - if ($join['alias'] === $joinAlias) { - $foundJoin = $join; - $foundJoinPath = sprintf('%s[%s]', $joinsPath, $key); - break; - } - } - - return [$foundJoin, $foundJoinPath]; - } - - /** - * @param DatagridConfiguration $config - * @param string $columnAlias - * - * @return array [select expression without column alias, select item path] - */ - private function findSelectExprByAlias(DatagridConfiguration $config, $columnAlias) - { - $foundSelectExpr = null; - $foundSelectPath = null; - $pattern = sprintf('#(?P.+?)\\s+AS\\s+%s#i', $columnAlias); - $selects = $config->offsetGetByPath('[source][query][select]', []); - foreach ($selects as $key => $select) { - if (preg_match($pattern, $select, $matches)) { - $foundSelectExpr = $matches['expr']; - $foundSelectPath = sprintf('[source][query][select][%s]', $key); - break; - } - } - - return [$foundSelectExpr, $foundSelectPath]; - } - - /** - * @param DatagridConfiguration $config - * - * @return string|null - */ - private function getRootAlias(DatagridConfiguration $config) - { - $fromPart = $config->offsetGetByPath('[source][query][from]', []); - if (empty($fromPart)) { - return null; - } - $from = reset($fromPart); - - return array_key_exists('alias', $from) - ? $from['alias'] - : null; - } } diff --git a/src/OroCRM/Bundle/MagentoBundle/Tests/Unit/EventListener/CustomerDataGridListenerTest.php b/src/OroCRM/Bundle/MagentoBundle/Tests/Unit/EventListener/CustomerDataGridListenerTest.php index 30b4bb69b29..dbc1480c7b5 100644 --- a/src/OroCRM/Bundle/MagentoBundle/Tests/Unit/EventListener/CustomerDataGridListenerTest.php +++ b/src/OroCRM/Bundle/MagentoBundle/Tests/Unit/EventListener/CustomerDataGridListenerTest.php @@ -162,107 +162,4 @@ public function testAddNewsletterSubscribersWhenFilteringByIsSubscriberWasReques $config->toArray() ); } - - public function testConvertJoinsToSubQueriesWhenSortingWasNotRequested() - { - $parameters = new ParameterBag(); - - $config = DatagridConfiguration::create( - [ - 'source' => [ - 'query' => [ - 'select' => [ - 'c.id', - 'dataChannel.name as channelName', - 'cw.name as websiteName', - 'cg.name as customerGroup' - ], - 'from' => [ - ['table' => 'OroCRM\Bundle\MagentoBundle\Entity\Customer', 'alias' => 'c'] - ], - 'join' => [ - 'left' => [ - ['join' => 'c.dataChannel', 'alias' => 'dataChannel'], - ['join' => 'c.website', 'alias' => 'cw'], - ['join' => 'c.group', 'alias' => 'cg'] - ] - ] - ] - ] - ] - ); - - $this->listener->onPreBuild(new PreBuild($config, $parameters)); - - $this->assertEquals( - [ - 'source' => [ - 'query' => [ - 'select' => [ - 'c.id', - '(SELECT dataChannel.name FROM OroCRM\Bundle\ChannelBundle\Entity\Channel AS dataChannel' - . ' WHERE dataChannel = c.dataChannel) AS channelName', - '(SELECT cw.name FROM OroCRM\Bundle\MagentoBundle\Entity\Website AS cw' - . ' WHERE cw = c.website) AS websiteName', - '(SELECT cg.name FROM OroCRM\Bundle\MagentoBundle\Entity\CustomerGroup AS cg' - . ' WHERE cg = c.group) AS customerGroup' - ], - 'from' => [ - ['table' => 'OroCRM\Bundle\MagentoBundle\Entity\Customer', 'alias' => 'c'] - ], - 'join' => [ - 'left' => [] - ] - ] - ] - ], - $config->toArray(['source']) - ); - } - - public function testConvertJoinsToSubQueriesWhenSortingWasRequested() - { - $parameters = new ParameterBag(); - $parameters->set( - '_sort_by', - [ - 'channelName' => '1', - 'websiteName' => '2', - 'customerGroup' => '3' - ] - ); - - $config = DatagridConfiguration::create( - [ - 'source' => [ - 'query' => [ - 'select' => [ - 'c.id', - 'dataChannel.name as channelName', - 'cw.name as websiteName', - 'cg.name as customerGroup' - ], - 'from' => [ - ['table' => 'OroCRM\Bundle\MagentoBundle\Entity\Customer', 'alias' => 'c'] - ], - 'join' => [ - 'left' => [ - ['join' => 'c.dataChannel', 'alias' => 'dataChannel'], - ['join' => 'c.website', 'alias' => 'cw'], - ['join' => 'c.group', 'alias' => 'cg'] - ] - ] - ] - ] - ] - ); - $originalConfig = $config->toArray(['source']); - - $this->listener->onPreBuild(new PreBuild($config, $parameters)); - - $this->assertEquals( - $originalConfig, - $config->toArray(['source']) - ); - } } diff --git a/src/OroCRM/Bundle/SalesBundle/Dashboard/Provider/OpportunityByStatusProvider.php b/src/OroCRM/Bundle/SalesBundle/Dashboard/Provider/OpportunityByStatusProvider.php index 854d9e66e51..2984bf471cc 100644 --- a/src/OroCRM/Bundle/SalesBundle/Dashboard/Provider/OpportunityByStatusProvider.php +++ b/src/OroCRM/Bundle/SalesBundle/Dashboard/Provider/OpportunityByStatusProvider.php @@ -4,13 +4,15 @@ use Symfony\Bridge\Doctrine\RegistryInterface; -use Doctrine\ORM\Query\Expr as Expr; +use Oro\Component\DoctrineUtils\ORM\QueryUtils; use Oro\Bundle\DashboardBundle\Filter\DateFilterProcessor; use Oro\Bundle\DashboardBundle\Model\WidgetOptionBag; +use Oro\Bundle\EntityExtendBundle\Entity\Repository\EnumValueRepository; +use Oro\Bundle\EntityExtendBundle\Tools\ExtendHelper; use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper; use Oro\Bundle\UserBundle\Dashboard\OwnerHelper; -use Oro\Component\DoctrineUtils\ORM\QueryUtils; + use OroCRM\Bundle\SalesBundle\Entity\Repository\OpportunityRepository; class OpportunityByStatusProvider @@ -54,10 +56,34 @@ public function getOpportunitiesGroupedByStatus(WidgetOptionBag $widgetOptions) { $dateRange = $widgetOptions->get('dateRange'); $owners = $this->ownerHelper->getOwnerIds($widgetOptions); + /** + * Excluded statuses will be filtered from result in method `formatResult` below. + * Due to performance issues with `NOT IN` clause in database. + */ $excludedStatuses = $widgetOptions->get('excluded_statuses', []); $orderBy = $widgetOptions->get('useQuantityAsData') ? 'quantity' : 'budget'; - $qb = $this->getOpportunityRepository() - ->getGroupedOpportunitiesByStatusQB('o', $orderBy); + + /** @var OpportunityRepository $opportunityRepository */ + $opportunityRepository = $this->registry->getRepository('OroCRMSalesBundle:Opportunity'); + $qb = $opportunityRepository->createQueryBuilder('o') + ->select('IDENTITY (o.status) status') + ->groupBy('status') + ->orderBy($orderBy, 'DESC'); + + switch ($orderBy) { + case 'quantity': + $qb->addSelect('COUNT(o.id) as quantity'); + break; + case 'budget': + $qb->addSelect( + 'SUM( + CASE WHEN o.status = \'won\' + THEN (CASE WHEN o.closeRevenue IS NOT NULL THEN o.closeRevenue ELSE 0 END) + ELSE (CASE WHEN o.budgetAmount IS NOT NULL THEN o.budgetAmount ELSE 0 END) + END + ) as budget' + ); + } $this->dateFilterProcessor->applyDateRangeFilterToQuery($qb, $dateRange, 'o.createdAt'); @@ -65,46 +91,59 @@ public function getOpportunitiesGroupedByStatus(WidgetOptionBag $widgetOptions) QueryUtils::applyOptimizedIn($qb, 'o.owner', $owners); } - // move previously applied conditions into join - // since we don't want to exclude any statuses from result - $joinConditions = $qb->getDQLPart('where'); - if ($joinConditions) { - $whereParts = (string) $joinConditions; - $qb->resetDQLPart('where'); - - $join = $qb->getDQLPart('join')['s'][0]; - $qb->resetDQLPart('join'); - - $qb->add( - 'join', - [ - 's' => new Expr\Join( - $join->getJoinType(), - $join->getJoin(), - $join->getAlias(), - $join->getConditionType(), - sprintf('%s AND (%s)', $join->getCondition(), $whereParts), - $join->getIndexBy() - ) - ], - true - ); - } + $result = $this->aclHelper->apply($qb)->getArrayResult(); + + return $this->formatResult($result, $excludedStatuses, $orderBy); + } - if ($excludedStatuses) { - $qb->andWhere( - $qb->expr()->notIn('s.id', $excludedStatuses) - ); + /** + * @param array $result + * @param string[] $excludedStatuses + * @param string $orderBy + * + * @return array + */ + protected function formatResult($result, $excludedStatuses, $orderBy) + { + $resultStatuses = array_flip(array_column($result, 'status', null)); + + foreach ($this->getAvailableOpportunityStatuses() as $statusKey => $statusLabel) { + $resultIndex = isset($resultStatuses[$statusKey]) ? $resultStatuses[$statusKey] : null; + if (in_array($statusKey, $excludedStatuses)) { + if (null !== $resultIndex) { + unset($result[$resultIndex]); + } + continue; + } + + if (null !== $resultIndex) { + $result[$resultIndex]['label'] = $statusLabel; + } else { + $result[] = [ + 'status' => $statusKey, + 'label' => $statusLabel, + $orderBy => 0 + ]; + } } - return $this->aclHelper->apply($qb)->getArrayResult(); + return $result; } /** - * @return OpportunityRepository + * @return array */ - protected function getOpportunityRepository() + protected function getAvailableOpportunityStatuses() { - return $this->registry->getRepository('OroCRMSalesBundle:Opportunity'); + /** @var EnumValueRepository $statusesRepository */ + $statusesRepository = $this->registry->getRepository( + ExtendHelper::buildEnumValueClassName('opportunity_status') + ); + $statuses = $statusesRepository->createQueryBuilder('s') + ->select('s.id, s.name') + ->getQuery() + ->getArrayResult(); + + return array_column($statuses, 'name', 'id'); } } diff --git a/src/OroCRM/Bundle/SalesBundle/Entity/B2bCustomer.php b/src/OroCRM/Bundle/SalesBundle/Entity/B2bCustomer.php index 57b1094a06b..1b82caef8af 100644 --- a/src/OroCRM/Bundle/SalesBundle/Entity/B2bCustomer.php +++ b/src/OroCRM/Bundle/SalesBundle/Entity/B2bCustomer.php @@ -22,7 +22,11 @@ /** * @ORM\Entity(repositoryClass="OroCRM\Bundle\SalesBundle\Entity\Repository\B2bCustomerRepository") - * @ORM\Table(name="orocrm_sales_b2bcustomer") + * @ORM\Table(name="orocrm_sales_b2bcustomer", indexes={ + * @ORM\Index( + * name="orocrm_b2bcustomer_name_idx", columns={"name", "id"} + * ) + * }) * @ORM\HasLifecycleCallbacks() * @Config( * routeName="orocrm_sales_b2bcustomer_index", diff --git a/src/OroCRM/Bundle/SalesBundle/Entity/Opportunity.php b/src/OroCRM/Bundle/SalesBundle/Entity/Opportunity.php index 5a30cfdf180..a4b5f7f158e 100644 --- a/src/OroCRM/Bundle/SalesBundle/Entity/Opportunity.php +++ b/src/OroCRM/Bundle/SalesBundle/Entity/Opportunity.php @@ -21,8 +21,14 @@ /** * @ORM\Entity(repositoryClass="OroCRM\Bundle\SalesBundle\Entity\Repository\OpportunityRepository") * @ORM\Table( - * name="orocrm_sales_opportunity", - * indexes={@ORM\Index(name="opportunity_created_idx",columns={"created_at", "id"})} + * name="orocrm_sales_opportunity", + * indexes={ + * @ORM\Index(name="opportunity_created_idx",columns={"created_at", "id"}), + * @ORM\Index( + * name="opportunities_by_status_idx", + * columns={"organization_id","status_id","close_revenue","budget_amount","created_at"} + * ) + * } * ) * @ORM\HasLifecycleCallbacks() * @Oro\Loggable diff --git a/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/OroCRMSalesBundleInstaller.php b/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/OroCRMSalesBundleInstaller.php index f2057b665eb..729a0917f77 100644 --- a/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/OroCRMSalesBundleInstaller.php +++ b/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/OroCRMSalesBundleInstaller.php @@ -100,7 +100,7 @@ public function setAttachmentExtension(AttachmentExtension $attachmentExtension) */ public function getMigrationVersion() { - return 'v1_25_7'; + return 'v1_25_8'; } /** @@ -120,6 +120,7 @@ public function up(Schema $schema, QueryBag $queries) $this->createOrocrmSalesLeadEmailTable($schema); $this->createOrocrmB2bCustomerPhoneTable($schema); $this->createOrocrmB2bCustomerEmailTable($schema); + $this->addB2bCustomerNameIndex($schema); /** Tables update */ $this->addOroEmailMailboxProcessorColumns($schema); @@ -152,6 +153,8 @@ public function up(Schema $schema, QueryBag $queries) $this->addOrocrmSalesOpportunityStatusField($schema, $queries); AddLeadStatus::addStatusField($schema, $this->extendExtension, $queries); AddLeadAddressTable::createLeadAddressTable($schema); + + $this->addOpportunitiesByStatusIndex($schema); } /** @@ -827,4 +830,29 @@ protected function addOrocrmSalesLeadEmailForeignKeys(Schema $schema) ['onDelete' => 'CASCADE', 'onUpdate' => null] ); } + + /** + * Add orocrm_sales_b2bcustomer index on field name + * + * @param Schema $schema + */ + protected function addB2bCustomerNameIndex(Schema $schema) + { + $table = $schema->getTable('orocrm_sales_b2bcustomer'); + $table->addIndex(['name', 'id'], 'orocrm_b2bcustomer_name_idx', []); + } + + /** + * Add opportunity 'opportunities_by_status_idx' index, used to speedup 'Opportunity By Status' widget + * + * @param Schema $schema + */ + protected function addOpportunitiesByStatusIndex(Schema $schema) + { + $table = $schema->getTable('orocrm_sales_opportunity'); + $table->addIndex( + ['organization_id', 'status_id', 'close_revenue', 'budget_amount', 'created_at'], + 'opportunities_by_status_idx' + ); + } } diff --git a/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/v1_25_7/AddB2bCustomerIndexOnName.php b/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/v1_25_7/AddB2bCustomerIndexOnName.php new file mode 100644 index 00000000000..01dd7e0b714 --- /dev/null +++ b/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/v1_25_7/AddB2bCustomerIndexOnName.php @@ -0,0 +1,20 @@ +getTable('orocrm_sales_b2bcustomer'); + $table->addIndex(['name', 'id'], 'orocrm_b2bcustomer_name_idx', []); + } +} diff --git a/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/v1_25_8/UpdateIndexes.php b/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/v1_25_8/UpdateIndexes.php new file mode 100644 index 00000000000..106f1299e9d --- /dev/null +++ b/src/OroCRM/Bundle/SalesBundle/Migrations/Schema/v1_25_8/UpdateIndexes.php @@ -0,0 +1,33 @@ +getTable('orocrm_sales_opportunity'); + $indexName = 'opportunities_by_status_idx'; + $indexColumns = [ + 'organization_id', + 'status_id', + 'close_revenue', + 'budget_amount', + 'created_at' + ]; + if ($table->hasIndex($indexName) && $table->getIndex($indexName)->getColumns() !== $indexColumns) { + $table->dropIndex($indexName); + $table->addIndex($indexColumns, $indexName); + } else { + $table->addIndex($indexColumns, $indexName); + } + } +} diff --git a/src/OroCRM/Bundle/SalesBundle/Provider/Opportunity/ForecastProvider.php b/src/OroCRM/Bundle/SalesBundle/Provider/Opportunity/ForecastProvider.php index efb661020c9..e5e65cf94cc 100644 --- a/src/OroCRM/Bundle/SalesBundle/Provider/Opportunity/ForecastProvider.php +++ b/src/OroCRM/Bundle/SalesBundle/Provider/Opportunity/ForecastProvider.php @@ -272,7 +272,7 @@ protected function applyDateFiltering( } if ($end) { $qb - ->andWhere(sprintf('%s < :end', $field)) + ->andWhere(sprintf('%s <= :end', $field)) ->setParameter('end', $end); } } diff --git a/src/OroCRM/Bundle/SalesBundle/Resources/config/oro/workflow/opportunity_flow/transitions.yml b/src/OroCRM/Bundle/SalesBundle/Resources/config/oro/workflow/opportunity_flow/transitions.yml index 7e22f6c8ca8..0d7b1c4e3be 100644 --- a/src/OroCRM/Bundle/SalesBundle/Resources/config/oro/workflow/opportunity_flow/transitions.yml +++ b/src/OroCRM/Bundle/SalesBundle/Resources/config/oro/workflow/opportunity_flow/transitions.yml @@ -96,6 +96,7 @@ workflows: - '@create_date': parameters: attribute: $close_date + - '@assign_value': [$close_revenue, $budget_amount] close_lost: label: 'Close as Lost' step_to: lost diff --git a/src/OroCRM/Bundle/SalesBundle/Tests/Unit/Dashboard/Provider/OpportunityByStatusProviderTest.php b/src/OroCRM/Bundle/SalesBundle/Tests/Unit/Dashboard/Provider/OpportunityByStatusProviderTest.php new file mode 100644 index 00000000000..c494d993a39 --- /dev/null +++ b/src/OroCRM/Bundle/SalesBundle/Tests/Unit/Dashboard/Provider/OpportunityByStatusProviderTest.php @@ -0,0 +1,358 @@ + 'won', 'name' => 'Won'], + ['id' => 'identification_alignment', 'name' => 'Identification'], + ['id' => 'in_progress', 'name' => 'Open'], + ['id' => 'needs_analysis', 'name' => 'Analysis'], + ['id' => 'negotiation', 'name' => 'Negotiation'], + ['id' => 'solution_development', 'name' => 'Development'], + ['id' => 'lost', 'name' => 'Lost'] + ]; + + /** @var OpportunityByStatusProvider */ + protected $provider; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + $this->registry = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry') + ->disableOriginalConstructor() + ->getMock(); + $this->aclHelper = $this->getMockBuilder('Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper') + ->disableOriginalConstructor() + ->getMock(); + $this->dateFilterProcessor = $this->getMockBuilder('Oro\Bundle\DashboardBundle\Filter\DateFilterProcessor') + ->disableOriginalConstructor() + ->getMock(); + $this->ownerHelper = $this->getMockBuilder('Oro\Bundle\UserBundle\Dashboard\OwnerHelper') + ->disableOriginalConstructor() + ->getMock(); + + $this->provider = new OpportunityByStatusProvider( + $this->registry, + $this->aclHelper, + $this->dateFilterProcessor, + $this->ownerHelper + ); + } + + /** + * @param WidgetOptionBag $widgetOptions + * @param string $expectation + * + * @dataProvider getOpportunitiesGroupedByStatusDQLDataProvider + */ + public function testGetOpportunitiesGroupedByStatusDQL($widgetOptions, $expectation) + { + $this->ownerHelper->expects($this->once()) + ->method('getOwnerIds') + ->willReturn([]); + + $opportunityQB = new QueryBuilder($this->getMock('Doctrine\ORM\EntityManagerInterface')); + $opportunityQB + ->from('OroCRM\Bundle\SalesBundle\Entity\Opportunity', 'o', null); + + $statusesQB = $this->getMockQueryBuilder(); + $statusesQB->expects($this->once()) + ->method('select') + ->with('s.id, s.name') + ->willReturnSelf(); + $statusesQB->expects($this->once()) + ->method('getQuery') + ->willReturnSelf(); + $statusesQB->expects($this->once()) + ->method('getArrayResult') + ->willReturn($this->opportunityStatuses); + + $repository = $this->getMockRepository(); + $repository->expects($this->exactly(2)) + ->method('createQueryBuilder') + ->withConsecutive(['o'], ['s']) + ->willReturnOnConsecutiveCalls($opportunityQB, $statusesQB); + + $this->registry->expects($this->exactly(2)) + ->method('getRepository') + ->withConsecutive( + ['OroCRMSalesBundle:Opportunity'], + [ExtendHelper::buildEnumValueClassName('opportunity_status')] + ) + ->willReturn($repository); + + $mockResult = $this->getMockQueryBuilder(); + $mockResult->expects($this->once()) + ->method('getArrayResult') + ->willReturn([]); + + $self = $this; + $this->aclHelper->expects($this->once()) + ->method('apply') + ->with( + $this->callback(function ($query) use ($self, $expectation) { + /** @var Query $query */ + $self->assertEquals($expectation, $query->getDQL()); + + return true; + }) + ) + ->willReturn($mockResult); + + $this->provider->getOpportunitiesGroupedByStatus($widgetOptions); + } + + public function getOpportunitiesGroupedByStatusDQLDataProvider() + { + return [ + 'request quantities' => [ + 'widgetOptions' => new WidgetOptionBag([ + 'excluded_statuses' => [], + 'useQuantityAsData' => true + ]), + 'expected DQL' => + 'SELECT IDENTITY (o.status) status, COUNT(o.id) as quantity ' + . 'FROM OroCRM\Bundle\SalesBundle\Entity\Opportunity o ' + . 'GROUP BY status ' + . 'ORDER BY quantity DESC' + ], + 'request quantities with excluded statuses - should not affect DQL' => [ + 'widgetOptions' => new WidgetOptionBag([ + 'excluded_statuses' => ['in_progress', 'won'], + 'useQuantityAsData' => true + ]), + 'expected DQL' => + 'SELECT IDENTITY (o.status) status, COUNT(o.id) as quantity ' + . 'FROM OroCRM\Bundle\SalesBundle\Entity\Opportunity o ' + . 'GROUP BY status ' + . 'ORDER BY quantity DESC' + ], + 'request budget amounts' => [ + 'widgetOptions' => new WidgetOptionBag([ + 'excluded_statuses' => [], + 'useQuantityAsData' => false + ]), + 'expected DQL' => << [ + 'widgetOptions' => new WidgetOptionBag([ + 'excluded_statuses' => ['in_progress', 'won'], + 'useQuantityAsData' => false + ]), + 'expected DQL' => <<ownerHelper->expects($this->once()) + ->method('getOwnerIds') + ->willReturn([]); + + $opportunityQB = new QueryBuilder($this->getMock('Doctrine\ORM\EntityManagerInterface')); + $opportunityQB + ->from('OroCRM\Bundle\SalesBundle\Entity\Opportunity', 'o', null); + + $statusesQB = $this->getMockQueryBuilder(); + $statusesQB->expects($this->once()) + ->method('select') + ->with('s.id, s.name') + ->willReturnSelf(); + $statusesQB->expects($this->once()) + ->method('getQuery') + ->willReturnSelf(); + $statusesQB->expects($this->once()) + ->method('getArrayResult') + ->willReturn($this->opportunityStatuses); + + $repository = $this->getMockRepository(); + $repository->expects($this->exactly(2)) + ->method('createQueryBuilder') + ->withConsecutive(['o'], ['s']) + ->willReturnOnConsecutiveCalls($opportunityQB, $statusesQB); + + $this->registry->expects($this->exactly(2)) + ->method('getRepository') + ->withConsecutive( + ['OroCRMSalesBundle:Opportunity'], + [ExtendHelper::buildEnumValueClassName('opportunity_status')] + ) + ->willReturn($repository); + + $mockResult = $this->getMockQueryBuilder(); + $mockResult->expects($this->once()) + ->method('getArrayResult') + ->willReturn($result); + + $this->aclHelper->expects($this->once()) + ->method('apply') + ->willReturn($mockResult); + + $data = $this->provider->getOpportunitiesGroupedByStatus($widgetOptions); + + $this->assertEquals($expected, $data); + } + + public function getOpportunitiesGroupedByStatusResultDataProvider() + { + return [ + 'result with all statuses, no exclusions - only labels should be added' => [ + 'widgetOptions' => new WidgetOptionBag([ + 'excluded_statuses' => [], + 'useQuantityAsData' => true + ]), + 'result data' => [ + 0 => ['quantity' => 700, 'status' => 'won'], + 1 => ['quantity' => 600, 'status' => 'identification_alignment'], + 2 => ['quantity' => 500, 'status' => 'in_progress'], + 3 => ['quantity' => 400, 'status' => 'needs_analysis'], + 4 => ['quantity' => 300, 'status' => 'negotiation'], + 5 => ['quantity' => 200, 'status' => 'solution_development'], + 6 => ['quantity' => 100, 'status' => 'lost'], + ], + 'expected formatted result' => [ + 0 => ['quantity' => 700, 'status' => 'won', 'label' => 'Won'], + 1 => ['quantity' => 600, 'status' => 'identification_alignment', 'label' => 'Identification'], + 2 => ['quantity' => 500, 'status' => 'in_progress', 'label' => 'Open'], + 3 => ['quantity' => 400, 'status' => 'needs_analysis', 'label' => 'Analysis'], + 4 => ['quantity' => 300, 'status' => 'negotiation', 'label' => 'Negotiation'], + 5 => ['quantity' => 200, 'status' => 'solution_development', 'label' => 'Development'], + 6 => ['quantity' => 100, 'status' => 'lost', 'label' => 'Lost'], + ] + ], + 'result with all statuses, with exclusions - excluded should be removed, labels' => [ + 'widgetOptions' => new WidgetOptionBag([ + 'excluded_statuses' => ['identification_alignment', 'solution_development'], + 'useQuantityAsData' => true + ]), + 'result data' => [ + 0 => ['quantity' => 700, 'status' => 'won'], + 1 => ['quantity' => 600, 'status' => 'identification_alignment'], + 2 => ['quantity' => 500, 'status' => 'in_progress'], + 3 => ['quantity' => 400, 'status' => 'needs_analysis'], + 4 => ['quantity' => 300, 'status' => 'negotiation'], + 5 => ['quantity' => 200, 'status' => 'solution_development'], + 6 => ['quantity' => 100, 'status' => 'lost'], + ], + 'expected formatted result' => [ + 0 => ['quantity' => 700, 'status' => 'won', 'label' => 'Won'], + 2 => ['quantity' => 500, 'status' => 'in_progress', 'label' => 'Open'], + 3 => ['quantity' => 400, 'status' => 'needs_analysis', 'label' => 'Analysis'], + 4 => ['quantity' => 300, 'status' => 'negotiation', 'label' => 'Negotiation'], + 6 => ['quantity' => 100, 'status' => 'lost', 'label' => 'Lost'], + ] + ], + 'result with NOT all statuses, no exclusions - all statuses, labels' => [ + 'widgetOptions' => new WidgetOptionBag([ + 'excluded_statuses' => [], + 'useQuantityAsData' => true + ]), + 'result data' => [ + 0 => ['quantity' => 700, 'status' => 'won'], + 1 => ['quantity' => 300, 'status' => 'negotiation'], + ], + 'expected formatted result' => [ + 0 => ['quantity' => 700, 'status' => 'won', 'label' => 'Won'], + 1 => ['quantity' => 300, 'status' => 'negotiation', 'label' => 'Negotiation'], + 2 => ['quantity' => 0, 'status' => 'identification_alignment', 'label' => 'Identification'], + 3 => ['quantity' => 0, 'status' => 'in_progress', 'label' => 'Open'], + 4 => ['quantity' => 0, 'status' => 'needs_analysis', 'label' => 'Analysis'], + 5 => ['quantity' => 0, 'status' => 'solution_development', 'label' => 'Development'], + 6 => ['quantity' => 0, 'status' => 'lost', 'label' => 'Lost'], + ] + ], + 'result with NOT all statuses AND exclusions - all statuses(except excluded), labels' => [ + 'widgetOptions' => new WidgetOptionBag([ + 'excluded_statuses' => ['identification_alignment', 'lost', 'in_progress'], + 'useQuantityAsData' => true + ]), + 'result data' => [ + 0 => ['quantity' => 700, 'status' => 'won'], + 1 => ['quantity' => 500, 'status' => 'in_progress'], + 2 => ['quantity' => 300, 'status' => 'negotiation'], + 3 => ['quantity' => 100, 'status' => 'lost'], + ], + 'expected formatted result' => [ + 0 => ['quantity' => 700, 'status' => 'won', 'label' => 'Won'], + 2 => ['quantity' => 300, 'status' => 'negotiation', 'label' => 'Negotiation'], + 4 => ['quantity' => 0, 'status' => 'needs_analysis', 'label' => 'Analysis'], + 5 => ['quantity' => 0, 'status' => 'solution_development', 'label' => 'Development'], + ] + ], + ]; + } + + /** + * @return EntityRepository|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getMockRepository() + { + return $this->getMockBuilder('Doctrine\ORM\EntityRepository') + ->disableOriginalConstructor() + ->setMethods(['createQueryBuilder']) + ->getMockForAbstractClass(); + } + + /** + * @return QueryBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getMockQueryBuilder() + { + return $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->disableOriginalConstructor() + ->setMethods(['select', 'where', 'setParameter', 'getQuery', 'getArrayResult']) + ->getMockForAbstractClass(); + } +}