Initial commit

This commit is contained in:
root
2026-01-19 17:44:46 +01:00
commit 823af8b11d
8721 changed files with 1130846 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Tools\Opportunity\Report;
use Espo\Core\Acl;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\Modules\Crm\Entities\Opportunity;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\Part\Expression;
use Espo\ORM\Query\Part\Order;
use RuntimeException;
use stdClass;
class ByLeadSource
{
public function __construct(
private Acl $acl,
private Config $config,
private Metadata $metadata,
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private Util $util
) {}
/**
* @throws Forbidden
*/
public function run(DateRange $range): stdClass
{
$range = $range->withFiscalYearShift(
$this->config->get('fiscalYearShift') ?? 0
);
if (!$this->acl->checkScope(Opportunity::ENTITY_TYPE, Acl\Table::ACTION_READ)) {
throw new Forbidden();
}
if (!$this->acl->checkField(Opportunity::ENTITY_TYPE, 'amount')) {
throw new Forbidden("No access to 'amount' field.");
}
[$from, $to] = $range->getRange();
$options = $this->metadata->get('entityDefs.Lead.fields.source.options', []);
try {
$queryBuilder = $this->selectBuilderFactory
->create()
->from(Opportunity::ENTITY_TYPE)
->withStrictAccessControl()
->buildQueryBuilder();
} catch (BadRequest|Forbidden $e) {
throw new RuntimeException($e->getMessage());
}
$whereClause = [
['stage!=' => $this->util->getLostStageList()],
['leadSource!=' => ''],
['leadSource!=' => null],
];
if ($from && $to) {
$whereClause[] = [
'closeDate>=' => $from->toString(),
'closeDate<' => $to->toString(),
];
}
if ($from && !$to) {
$whereClause[] = [
'closeDate>=' => $from->toString(),
];
}
if (!$from && $to) {
$whereClause[] = [
'closeDate<' => $to->toString(),
];
}
$queryBuilder
->select([
'leadSource',
['SUM:amountWeightedConverted', 'amount'],
])
->order(
Order::createByPositionInList(
Expression::column('leadSource'),
$options
)
)
->group('leadSource')
->where($whereClause);
$this->util->handleDistinctReportQueryBuilder($queryBuilder, $whereClause);
$sth = $this->entityManager
->getQueryExecutor()
->execute($queryBuilder->build());
$rowList = $sth->fetchAll() ?: [];
$result = [];
foreach ($rowList as $row) {
$leadSource = $row['leadSource'];
$result[$leadSource] = floatval($row['amount']);
}
return (object) $result;
}
}

View File

@@ -0,0 +1,171 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Tools\Opportunity\Report;
use Espo\Core\Acl;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\Modules\Crm\Entities\Opportunity;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\Part\Expression;
use Espo\ORM\Query\Part\Order;
use stdClass;
class ByStage
{
private Acl $acl;
private Config $config;
private Metadata $metadata;
private EntityManager $entityManager;
private SelectBuilderFactory $selectBuilderFactory;
private Util $util;
public function __construct(
Acl $acl,
Config $config,
Metadata $metadata,
EntityManager $entityManager,
SelectBuilderFactory $selectBuilderFactory,
Util $util
) {
$this->acl = $acl;
$this->config = $config;
$this->metadata = $metadata;
$this->entityManager = $entityManager;
$this->selectBuilderFactory = $selectBuilderFactory;
$this->util = $util;
}
/**
* @throws Forbidden
*/
public function run(DateRange $range): stdClass
{
$range = $range->withFiscalYearShift(
$this->config->get('fiscalYearShift') ?? 0
);
if (!$this->acl->checkScope(Opportunity::ENTITY_TYPE, Acl\Table::ACTION_READ)) {
throw new Forbidden();
}
if (!$this->acl->checkField(Opportunity::ENTITY_TYPE, 'amount')) {
throw new Forbidden("No access to 'amount' field.");
}
[$from, $to] = $range->getRange();
$options = $this->metadata->get('entityDefs.Opportunity.fields.stage.options') ?? [];
$queryBuilder = $this->selectBuilderFactory
->create()
->from(Opportunity::ENTITY_TYPE)
->withStrictAccessControl()
->buildQueryBuilder();
$whereClause = [
['stage!=' => $this->util->getLostStageList()],
['stage!=' => $this->util->getWonStageList()],
];
if ($from && $to) {
$whereClause[] = [
'closeDate>=' => $from->toString(),
'closeDate<' => $to->toString(),
];
}
if ($from && !$to) {
$whereClause[] = [
'closeDate>=' => $from->toString(),
];
}
if (!$from && $to) {
$whereClause[] = [
'closeDate<' => $to->toString(),
];
}
$queryBuilder
->select([
'stage',
['SUM:amountConverted', 'amount'],
])
->order(
Order::createByPositionInList(
Expression::column('stage'),
$options
)
)
->group('stage')
->where($whereClause);
$stageIgnoreList = array_merge(
$this->util->getLostStageList(),
$this->util->getWonStageList()
);
$this->util->handleDistinctReportQueryBuilder($queryBuilder, $whereClause);
$sth = $this->entityManager
->getQueryExecutor()
->execute($queryBuilder->build());
$rowList = $sth->fetchAll() ?: [];
$result = [];
foreach ($rowList as $row) {
$stage = $row['stage'];
if (in_array($stage, $stageIgnoreList)) {
continue;
}
$result[$stage] = floatval($row['amount']);
}
foreach ($options as $stage) {
if (in_array($stage, $stageIgnoreList)) {
continue;
}
if (array_key_exists($stage, $result)) {
continue;
}
$result[$stage] = 0.0;
}
return (object) $result;
}
}

View File

@@ -0,0 +1,177 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Tools\Opportunity\Report;
use DateInterval;
use Espo\Core\Field\Date;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* Immutable.
*/
class DateRange
{
public const TYPE_BETWEEN = 'between';
public const TYPE_EVER = 'ever';
public const TYPE_CURRENT_YEAR = 'currentYear';
public const TYPE_CURRENT_QUARTER = 'currentQuarter';
public const TYPE_CURRENT_MONTH = 'currentMonth';
public const TYPE_CURRENT_FISCAL_YEAR = 'currentFiscalYear';
public const TYPE_CURRENT_FISCAL_QUARTER = 'currentFiscalQuarter';
private string $type;
private ?Date $from;
private ?Date $to;
private int $fiscalYearShift;
public function __construct(
string $type,
?Date $from = null,
?Date $to = null,
int $fiscalYearShift = 0
) {
if ($type === self::TYPE_BETWEEN && (!$from || !$to)) {
throw new InvalidArgumentException("Missing range dates.");
}
$this->type = $type;
$this->from = $from;
$this->to = $to;
$this->fiscalYearShift = $fiscalYearShift;
}
public function getType(): string
{
return $this->type;
}
public function getFrom(): ?Date
{
return $this->from;
}
public function getTo(): ?Date
{
return $this->to;
}
public function withFiscalYearShift(int $fiscalYearShift): self
{
$obj = clone $this;
$obj->fiscalYearShift = $fiscalYearShift;
return $obj;
}
/**
* @return array{?Date, ?Date}
*/
public function getRange(): array
{
if ($this->type === self::TYPE_EVER) {
return [null, null];
}
if ($this->type === self::TYPE_BETWEEN) {
return [$this->from, $this->to];
}
$fiscalYearShift = $this->fiscalYearShift;
switch ($this->type) {
case self::TYPE_CURRENT_YEAR:
$dt = Date::createToday()
->modify('first day of January this year');
return [
$dt,
$dt->addYears(1)
];
case self::TYPE_CURRENT_QUARTER:
$dt = Date::createToday();
$quarter = (int) ceil($dt->getMonth() / 3);
$dt = $dt
->modify('first day of January this year')
->addMonths(($quarter - 1) * 3);
return [
$dt,
$dt->addMonths(3),
];
case self::TYPE_CURRENT_MONTH:
$dt = Date::createToday()
->modify('first day of this month');
return [
$dt,
$dt->addMonths(1),
];
case self::TYPE_CURRENT_FISCAL_YEAR:
$dt = Date::createToday()
->modify('first day of January this year')
->modify('+' . $fiscalYearShift . ' months');
if (Date::createToday()->getMonth() < $fiscalYearShift + 1) {
$dt = $dt->addYears(-1);
}
return [
$dt,
$dt->addYears(1)
];
case self::TYPE_CURRENT_FISCAL_QUARTER:
$dt = Date::createToday()
->modify('first day of January this year')
->addMonths($fiscalYearShift);
$month = Date::createToday()->getMonth();
$quarterShift = (int) floor(($month - $fiscalYearShift - 1) / 3);
if ($quarterShift) {
$dt = $dt->addMonths($quarterShift * 3);
}
return [
$dt,
$dt->add(new DateInterval('P3M'))
];
}
throw new UnexpectedValueException("Not supported range type");
}
}

View File

@@ -0,0 +1,174 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Tools\Opportunity\Report;
use DateTime;
use Espo\Core\Acl;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\Core\Utils\Config;
use Espo\Modules\Crm\Entities\Opportunity;
use Espo\ORM\EntityManager;
use Exception;
use InvalidArgumentException;
use LogicException;
use stdClass;
class SalesByMonth
{
public function __construct(
private Acl $acl,
private Config $config,
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private Util $util
) {}
/**
* @throws Forbidden
*/
public function run(DateRange $range): stdClass
{
$range = $range->withFiscalYearShift(
$this->config->get('fiscalYearShift') ?? 0
);
if (!$this->acl->checkScope(Opportunity::ENTITY_TYPE, Acl\Table::ACTION_READ)) {
throw new Forbidden();
}
if (!$this->acl->checkField(Opportunity::ENTITY_TYPE, 'amount')) {
throw new Forbidden("No access to 'amount' field.");
}
[$from, $to] = $range->getRange();
if (!$from || !$to) {
throw new InvalidArgumentException();
}
$queryBuilder = $this->selectBuilderFactory
->create()
->from(Opportunity::ENTITY_TYPE)
->withStrictAccessControl()
->buildQueryBuilder();
$whereClause = [
'stage' => $this->util->getWonStageList(),
];
$whereClause[] = [
'closeDate>=' => $from->toString(),
'closeDate<' => $to->toString(),
];
$queryBuilder
->select([
['MONTH:closeDate', 'month'],
['SUM:amountConverted', 'amount'],
])
->order('MONTH:closeDate')
->group('MONTH:closeDate')
->where($whereClause);
$this->util->handleDistinctReportQueryBuilder($queryBuilder, $whereClause);
$sth = $this->entityManager
->getQueryExecutor()
->execute($queryBuilder->build());
$result = [];
$rowList = $sth->fetchAll() ?: [];
foreach ($rowList as $row) {
$month = $row['month'];
$result[$month] = floatval($row['amount']);
}
$dt = $from;
$dtTo = $to;
if ($dtTo->getDay() > 1) {
$dtTo = $dtTo
->addDays(1 - $dtTo->getDay()) // First day of month.
->addMonths(1);
} else {
$dtTo = $dtTo->addDays(1 - $dtTo->getDay());
}
while ($dt->toTimestamp() < $dtTo->toTimestamp()) {
$month = $dt->toDateTime()->format('Y-m');
if (!array_key_exists($month, $result)) {
$result[$month] = 0;
}
$dt = $dt->addMonths(1);
}
$keyList = array_keys($result);
sort($keyList);
$today = new DateTime();
$endPosition = count($keyList);
for ($i = count($keyList) - 1; $i >= 0; $i--) {
$key = $keyList[$i];
try {
$dt = new DateTime($key . '-01');
} catch (Exception $e) {
throw new LogicException();
}
if ($dt->getTimestamp() < $today->getTimestamp()) {
break;
}
if (empty($result[$key])) {
$endPosition = $i;
continue;
}
break;
}
$keyListSliced = array_slice($keyList, 0, $endPosition);
return (object) [
'keyList' => $keyListSliced,
'dataMap' => $result,
];
}
}

View File

@@ -0,0 +1,177 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Tools\Opportunity\Report;
use Espo\Core\Acl;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Name\Field;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\Modules\Crm\Entities\Opportunity;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\Part\Expression;
use Espo\ORM\Query\Part\Order;
use stdClass;
class SalesPipeline
{
public function __construct(
private Acl $acl,
private Config $config,
private Metadata $metadata,
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private Util $util
) {}
/**
* @throws Forbidden
*/
public function run(DateRange $range, bool $useLastStage = false, ?string $teamId = null): stdClass
{
$range = $range->withFiscalYearShift(
$this->config->get('fiscalYearShift') ?? 0
);
if (!$this->acl->checkScope(Opportunity::ENTITY_TYPE, Acl\Table::ACTION_READ)) {
throw new Forbidden();
}
if (!$this->acl->checkField(Opportunity::ENTITY_TYPE, 'amount')) {
throw new Forbidden("No access to 'amount' field.");
}
[$from, $to] = $range->getRange();
$lostStageList = $this->util->getLostStageList();
$options = $this->metadata->get('entityDefs.Opportunity.fields.stage.options', []);
$queryBuilder = $this->selectBuilderFactory
->create()
->from(Opportunity::ENTITY_TYPE)
->withStrictAccessControl()
->buildQueryBuilder();
$stageField = 'stage';
if ($useLastStage) {
$stageField = 'lastStage';
}
$whereClause = [
[$stageField . '!=' => $lostStageList],
[$stageField . '!=' => null],
];
if ($from && $to) {
$whereClause[] = [
'closeDate>=' => $from->toString(),
'closeDate<' => $to->toString(),
];
}
if ($from && !$to) {
$whereClause[] = [
'closeDate>=' => $from->toString(),
];
}
if (!$from && $to) {
$whereClause[] = [
'closeDate<' => $to->toString(),
];
}
if ($teamId) {
$whereClause[] = [
'teamsFilter.id' => $teamId,
];
}
$queryBuilder
->select([
$stageField,
['SUM:amountConverted', 'amount'],
])
->order(
Order::createByPositionInList(
Expression::column($stageField),
$options
)
)
->group($stageField)
->where($whereClause);
if ($teamId) {
$queryBuilder->join(Field::TEAMS, 'teamsFilter');
}
$this->util->handleDistinctReportQueryBuilder($queryBuilder, $whereClause);
$sth = $this->entityManager
->getQueryExecutor()
->execute($queryBuilder->build());
$rowList = $sth->fetchAll() ?: [];
$data = [];
foreach ($rowList as $row) {
$stage = $row[$stageField];
$data[$stage] = floatval($row['amount']);
}
$dataList = [];
$stageList = $this->metadata->get('entityDefs.Opportunity.fields.stage.options', []);
foreach ($stageList as $stage) {
if (in_array($stage, $lostStageList)) {
continue;
}
if (!in_array($stage, $lostStageList) && !isset($data[$stage])) {
$data[$stage] = 0.0;
}
$dataList[] = [
'stage' => $stage,
'value' => $data[$stage],
];
}
return (object) [
'dataList' => $dataList,
];
}
}

View File

@@ -0,0 +1,120 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Tools\Opportunity\Report;
use Espo\Core\Utils\Metadata;
use Espo\Modules\Crm\Entities\Opportunity as OpportunityEntity;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\SelectBuilder;
class Util
{
private Metadata $metadata;
private EntityManager $entityManager;
public function __construct(
Metadata $metadata,
EntityManager $entityManager
) {
$this->metadata = $metadata;
$this->entityManager = $entityManager;
}
/**
* A grouping-by with distinct will give wrong results. Need to use sub-query.
*
* @param array<string|int, mixed> $whereClause
*/
public function handleDistinctReportQueryBuilder(SelectBuilder $queryBuilder, array $whereClause): void
{
if (!$queryBuilder->build()->isDistinct()) {
return;
}
$subQuery = $this->entityManager
->getQueryBuilder()
->select()
->from(OpportunityEntity::ENTITY_TYPE)
->select(Attribute::ID)
->where($whereClause)
->build();
$queryBuilder->where([
'id=s' => $subQuery,
]);
}
/**
* @return string[]
*/
public function getLostStageList(): array
{
$list = [];
$probabilityMap = $this->metadata
->get(['entityDefs', OpportunityEntity::ENTITY_TYPE, 'fields', 'stage', 'probabilityMap']) ?? [];
$stageList = $this->metadata->get('entityDefs.Opportunity.fields.stage.options', []);
foreach ($stageList as $stage) {
$value = $probabilityMap[$stage] ?? 0;
if (!$value) {
$list[] = $stage;
}
}
return $list;
}
/**
* @return string[]
*/
public function getWonStageList(): array
{
$list = [];
$probabilityMap = $this->metadata
->get(['entityDefs', OpportunityEntity::ENTITY_TYPE, 'fields', 'stage', 'probabilityMap']) ?? [];
$stageList = $this->metadata->get('entityDefs.Opportunity.fields.stage.options', []);
foreach ($stageList as $stage) {
$value = $probabilityMap[$stage] ?? 0;
if ($value == 100) {
$list[] = $stage;
}
}
return $list;
}
}