From c1eb14726e9628a9581a8b5cb97fe1458e36a507 Mon Sep 17 00:00:00 2001 From: Yurii Date: Thu, 19 Feb 2026 10:09:32 +0200 Subject: [PATCH] currency no join mode getting rates dynamically --- .../Part/FunctionConverters/CurrencyRate.php | 96 +++++++++++++++++++ .../Database/Orm/FieldConverters/Currency.php | 35 +------ .../Espo/Resources/metadata/app/orm.json | 6 +- .../Espo/Currency/CurrencyTest.php | 85 ++++++++++++++++ 4 files changed, 189 insertions(+), 33 deletions(-) create mode 100644 application/Espo/Core/ORM/QueryComposer/Part/FunctionConverters/CurrencyRate.php diff --git a/application/Espo/Core/ORM/QueryComposer/Part/FunctionConverters/CurrencyRate.php b/application/Espo/Core/ORM/QueryComposer/Part/FunctionConverters/CurrencyRate.php new file mode 100644 index 0000000000..4dcf0f8125 --- /dev/null +++ b/application/Espo/Core/ORM/QueryComposer/Part/FunctionConverters/CurrencyRate.php @@ -0,0 +1,96 @@ +. + * + * 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\Core\ORM\QueryComposer\Part\FunctionConverters; + +use Espo\Core\Currency\ConfigDataProvider; +use Espo\ORM\Query\Part\Expression; +use Espo\ORM\QueryComposer\Part\FunctionConverter; +use Espo\ORM\QueryComposer\Util; +use RuntimeException; + +/** + * @noinspection PhpUnused + */ +class CurrencyRate implements FunctionConverter +{ + private const int PRECISION = 5; + + public function __construct( + private ConfigDataProvider $config, + ) {} + + public function convert(string ...$argumentList): string + { + $arg = $argumentList[0] ?? null; + + if (!is_string($arg) || !Util::isArgumentString($arg)) { + throw new RuntimeException("CURRENCY_RATE function accepts only literal string argument."); + } + + $code = substr($arg, 1, -1); + + if (!in_array($code, $this->config->getCurrencyList())) { + return Expression::value(0)->getValue(); + } + + $baseCurrency = $this->config->getBaseCurrency(); + $defaultCurrency = $this->config->getDefaultCurrency(); + + $rates = $this->config->getCurrencyRates()->toAssoc(); + + if ($defaultCurrency !== $baseCurrency) { + $rates = $this->exchangeRates($baseCurrency, $defaultCurrency, $rates); + } + + $rate = $rates[$code] ?? 1.0; + + return Expression::value($rate)->getValue(); + } + + /** + * @param array $currencyRates + * @return array + */ + private function exchangeRates(string $baseCurrency, string $defaultCurrency, array $currencyRates): array + { + $defaultCurrencyRate = round(1 / $currencyRates[$defaultCurrency], self::PRECISION); + + $exchangedRates = []; + $exchangedRates[$baseCurrency] = $defaultCurrencyRate; + + unset($currencyRates[$baseCurrency], $currencyRates[$defaultCurrency]); + + foreach ($currencyRates as $code => $rate) { + $exchangedRates[$code] = round($rate * $defaultCurrencyRate, self::PRECISION); + } + + return $exchangedRates; + } +} diff --git a/application/Espo/Core/Utils/Database/Orm/FieldConverters/Currency.php b/application/Espo/Core/Utils/Database/Orm/FieldConverters/Currency.php index d473aefa16..46fa5ec042 100644 --- a/application/Espo/Core/Utils/Database/Orm/FieldConverters/Currency.php +++ b/application/Espo/Core/Utils/Database/Orm/FieldConverters/Currency.php @@ -124,12 +124,8 @@ class Currency implements FieldConverter $currencyAttribute = $name . 'Currency'; $defaultCurrency = $this->configDataProvider->getDefaultCurrency(); - $baseCurrency = $this->configDataProvider->getBaseCurrency(); - $rates = $this->configDataProvider->getCurrencyRates()->toAssoc(); - if ($defaultCurrency !== $baseCurrency) { - $rates = $this->exchangeRates($baseCurrency, $defaultCurrency, $rates); - } + $rates = $this->configDataProvider->getCurrencyList(); $expr = Expr::multiply( Expr::column($name), @@ -216,28 +212,7 @@ class Currency implements FieldConverter } /** - * @param array $currencyRates - * @return array - */ - private function exchangeRates(string $baseCurrency, string $defaultCurrency, array $currencyRates): array - { - $precision = 5; - $defaultCurrencyRate = round(1 / $currencyRates[$defaultCurrency], $precision); - - $exchangedRates = []; - $exchangedRates[$baseCurrency] = $defaultCurrencyRate; - - unset($currencyRates[$baseCurrency], $currencyRates[$defaultCurrency]); - - foreach ($currencyRates as $currencyName => $rate) { - $exchangedRates[$currencyName] = round($rate * $defaultCurrencyRate, $precision); - } - - return $exchangedRates; - } - - /** - * @param array $rates + * @param string[] $rates */ private function buildExpression(string $currencyAttribute, array $rates): Expr|float { @@ -245,13 +220,11 @@ class Currency implements FieldConverter return 0.0; } - $currency = array_key_first($rates); - $value = $rates[$currency]; - unset($rates[$currency]); + $currency = array_shift($rates); return Expr::if( Expr::equal(Expr::column($currencyAttribute), $currency), - $value, + Expr::create("CURRENCY_RATE:('$currency')"), $this->buildExpression($currencyAttribute, $rates) ); } diff --git a/application/Espo/Resources/metadata/app/orm.json b/application/Espo/Resources/metadata/app/orm.json index 94187d88b9..beaab09ab3 100644 --- a/application/Espo/Resources/metadata/app/orm.json +++ b/application/Espo/Resources/metadata/app/orm.json @@ -4,14 +4,16 @@ "queryComposerClassName": "Espo\\ORM\\QueryComposer\\MysqlQueryComposer", "pdoFactoryClassName": "Espo\\ORM\\PDO\\MysqlPDOFactory", "functionConverterClassNameMap": { - "ABS": "Espo\\Core\\ORM\\QueryComposer\\Part\\FunctionConverters\\Abs" + "ABS": "Espo\\Core\\ORM\\QueryComposer\\Part\\FunctionConverters\\Abs", + "CURRENCY_RATE": "Espo\\Core\\ORM\\QueryComposer\\Part\\FunctionConverters\\CurrencyRate" } }, "Postgresql": { "queryComposerClassName": "Espo\\ORM\\QueryComposer\\PostgresqlQueryComposer", "pdoFactoryClassName": "Espo\\ORM\\PDO\\PostgresqlPDOFactory", "functionConverterClassNameMap": { - "ABS": "Espo\\Core\\ORM\\QueryComposer\\Part\\FunctionConverters\\Abs" + "ABS": "Espo\\Core\\ORM\\QueryComposer\\Part\\FunctionConverters\\Abs", + "CURRENCY_RATE": "Espo\\Core\\ORM\\QueryComposer\\Part\\FunctionConverters\\CurrencyRate" } } } diff --git a/tests/integration/Espo/Currency/CurrencyTest.php b/tests/integration/Espo/Currency/CurrencyTest.php index 2299fd0f57..b0beaefdd6 100644 --- a/tests/integration/Espo/Currency/CurrencyTest.php +++ b/tests/integration/Espo/Currency/CurrencyTest.php @@ -33,6 +33,7 @@ use Espo\Core\Exceptions\Error; use Espo\Core\Field\Date; use Espo\Core\Formula\Manager as FormulaManager; use Espo\Modules\Crm\Entities\Lead; +use Espo\Modules\Crm\Entities\Opportunity; use Espo\Tools\Currency\RateEntryProvider; use Espo\Tools\Currency\RateService; use Espo\Core\Currency\Rates; @@ -178,4 +179,88 @@ class CurrencyTest extends BaseTestCase $value = $formulaManager->run($script); $this->assertEquals(1.0, (float) $value); } + + /** + * @noinspection PhpUnhandledExceptionInspection + */ + public function testDefaultCurrencyJoinMode(): void + { + $configWriter = $this->getInjectableFactory()->create(ConfigWriter::class); + + $configWriter->set('currencyNoJoinMode', false); + $configWriter->set('currencyList', ['USD', 'EUR']); + $configWriter->set('defaultCurrency', 'USD'); + $configWriter->set('baseCurrency', 'EUR'); + $configWriter->save(); + + $this->getInjectableFactory()->create(SyncManager::class)->sync(); + + $rateService = $this->getInjectableFactory()->create(RateService::class); + $rateService->set(Rates::fromAssoc([ + 'USD' => 0.5, + ], 'EUR')); + + $this->getDataManager()->rebuild(); + $this->reCreateApplication(); + + $em = $this->getEntityManager(); + + $opp = $em->getRDBRepositoryByClass(Opportunity::class)->getNew(); + $opp->setAmount(Currency::create('10.0', 'USD')); + $em->saveEntity($opp); + $em->refreshEntity($opp); + + $this->assertEquals(10, $opp->get('amountConverted')); + + // + + $opp = $em->getRDBRepositoryByClass(Opportunity::class)->getNew(); + $opp->setAmount(Currency::create('10.0', 'EUR')); + $em->saveEntity($opp); + $em->refreshEntity($opp); + + $this->assertEquals(20, $opp->get('amountConverted')); + } + + /** + * @noinspection PhpUnhandledExceptionInspection + */ + public function testDefaultCurrencyNoJoinMode(): void + { + $configWriter = $this->getInjectableFactory()->create(ConfigWriter::class); + + $configWriter->set('currencyNoJoinMode', true); + $configWriter->set('currencyList', ['USD', 'EUR']); + $configWriter->set('defaultCurrency', 'USD'); + $configWriter->set('baseCurrency', 'EUR'); + $configWriter->save(); + + $this->getInjectableFactory()->create(SyncManager::class)->sync(); + + $rateService = $this->getInjectableFactory()->create(RateService::class); + $rateService->set(Rates::fromAssoc([ + 'USD' => 0.5, + ], 'EUR')); + + $this->getDataManager()->rebuild(); + $this->reCreateApplication(); + + $em = $this->getEntityManager(); + + $opp = $em->getRDBRepositoryByClass(Opportunity::class)->getNew(); + $opp->setAmount(Currency::create('10.0', 'USD')); + $em->saveEntity($opp); + $em->refreshEntity($opp); + + $this->assertEquals(10, $opp->get('amountConverted')); + + // + + $opp = $em->getRDBRepositoryByClass(Opportunity::class)->getNew(); + $opp->setAmount(Currency::create('10.0', 'EUR')); + $em->saveEntity($opp); + $em->refreshEntity($opp); + + $this->assertEquals(20, $opp->get('amountConverted')); + } }