diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index a4c63b2952..ebbc52ff2b 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -7,6 +7,7 @@
+
@@ -19,6 +20,8 @@
+
+
diff --git a/application/Espo/Core/Currency/CalculatorUtil.php b/application/Espo/Core/Currency/CalculatorUtil.php
index 275603401f..68be741417 100644
--- a/application/Espo/Core/Currency/CalculatorUtil.php
+++ b/application/Espo/Core/Currency/CalculatorUtil.php
@@ -33,7 +33,7 @@ use DivisionByZeroError;
class CalculatorUtil
{
- private const SCALE = 14;
+ private const int SCALE = 14;
/**
* @param numeric-string $arg1
@@ -43,16 +43,10 @@ class CalculatorUtil
public static function add(string $arg1, string $arg2): string
{
if (!function_exists('bcadd')) {
- return (string) (
- (float) $arg1 + (float) $arg2
- );
+ return self::floatToString((float) $arg1 + (float) $arg2);
}
- return bcadd(
- $arg1,
- $arg2,
- self::SCALE
- );
+ return bcadd($arg1, $arg2, self::SCALE);
}
/**
@@ -63,56 +57,42 @@ class CalculatorUtil
public static function subtract(string $arg1, string $arg2): string
{
if (!function_exists('bcsub')) {
- return (string) (
- (float) $arg1 - (float) $arg2
- );
+ return self::floatToString((float) $arg1 - (float) $arg2);
}
- return bcsub(
- $arg1,
- $arg2,
- self::SCALE
- );
+ return bcsub($arg1, $arg2, self::SCALE);
}
/**
* @param numeric-string $arg1
* @param numeric-string $arg2
* @return numeric-string
+ *
+ * @todo For the result, trim right zeros. Then, trim dot.
*/
public static function multiply(string $arg1, string $arg2): string
{
if (!function_exists('bcmul')) {
- return (string) (
- (float) $arg1 * (float) $arg2
- );
+ return self::floatToString((float) $arg1 * (float) $arg2);
}
- return bcmul(
- $arg1,
- $arg2,
- self::SCALE
- );
+ return bcmul($arg1, $arg2, self::SCALE);
}
/**
* @param numeric-string $arg1
* @param numeric-string $arg2
* @return numeric-string
+ *
+ * @todo For the result, trim right zeros. Then, trim dot.
*/
public static function divide(string $arg1, string $arg2): string
{
if (!function_exists('bcdiv')) {
- return (string) (
- (float) $arg1 / (float) $arg2
- );
+ return self::floatToString((float) $arg1 / (float) $arg2);
}
- $result = bcdiv(
- $arg1,
- $arg2,
- self::SCALE
- );
+ $result = bcdiv($arg1, $arg2, self::SCALE);
if ($result === null) { /** @phpstan-ignore-line */
throw new DivisionByZeroError();
@@ -128,7 +108,9 @@ class CalculatorUtil
public static function round(string $arg, int $precision = 0): string
{
if (!function_exists('bcadd')) {
- return (string) round((float) $arg, $precision);
+ return self::floatToString(
+ round((float) $arg, $precision)
+ );
}
$addition = '0.' . str_repeat('0', $precision) . '5';
@@ -139,11 +121,7 @@ class CalculatorUtil
assert(is_numeric($addition));
- return bcadd(
- $arg,
- $addition,
- $precision
- );
+ return bcadd($arg, $addition, $precision);
}
/**
@@ -156,10 +134,15 @@ class CalculatorUtil
return (float) $arg1 <=> (float) $arg2;
}
- return bccomp(
- $arg1,
- $arg2,
- self::SCALE
- );
+ return bccomp($arg1, $arg2, self::SCALE);
+ }
+
+ /**
+ * @return numeric-string
+ */
+ private static function floatToString(float $amount): string
+ {
+ /** @var numeric-string */
+ return rtrim(rtrim(sprintf('%.' . self::SCALE . 'f', $amount), '0'), '.');
}
}
diff --git a/application/Espo/Core/Field/Currency.php b/application/Espo/Core/Field/Currency.php
index e00350838d..81da1789b8 100644
--- a/application/Espo/Core/Field/Currency.php
+++ b/application/Espo/Core/Field/Currency.php
@@ -38,6 +38,8 @@ use InvalidArgumentException;
*/
class Currency
{
+ private const int DEFAULT_SCALE = 14;
+
/** @var numeric-string */
private string $amount;
private string $code;
@@ -57,8 +59,10 @@ class Currency
throw new InvalidArgumentException("Bad currency code.");
}
- if (is_float($amount) || is_int($amount)) {
+ if (is_int($amount)) {
$amount = (string) $amount;
+ } else if (is_float($amount)) {
+ $amount = self::floatToString($amount);
}
$this->amount = $amount;
@@ -136,10 +140,9 @@ class Currency
*/
public function multiply(float|int|string $multiplier): self
{
- $amount = CalculatorUtil::multiply(
- $this->getAmountAsString(),
- (string) $multiplier
- );
+ $multiplier = is_float($multiplier) ? self::floatToString($multiplier) : (string) $multiplier;
+
+ $amount = CalculatorUtil::multiply($this->getAmountAsString(), $multiplier);
return new self($amount, $this->getCode());
}
@@ -151,10 +154,9 @@ class Currency
*/
public function divide(float|int|string $divider): self
{
- $amount = CalculatorUtil::divide(
- $this->getAmountAsString(),
- (string) $divider
- );
+ $divider = is_float($divider) ? self::floatToString($divider) : (string) $divider;
+
+ $amount = CalculatorUtil::divide($this->getAmountAsString(), $divider);
return new self($amount, $this->getCode());
}
@@ -208,4 +210,13 @@ class Currency
{
return new self($amount, $code);
}
+
+ /**
+ * @return numeric-string
+ */
+ private static function floatToString(float $amount): string
+ {
+ /** @var numeric-string */
+ return rtrim(rtrim(sprintf('%.' . self::DEFAULT_SCALE . 'f', $amount), '0'), '.');
+ }
}
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/client/src/views/role/record/edit.js b/client/src/views/role/record/edit.js
index ddcd2cdbb1..b30b5fdd2f 100644
--- a/client/src/views/role/record/edit.js
+++ b/client/src/views/role/record/edit.js
@@ -56,7 +56,7 @@ class RoleEditRecordView extends EditRecordView {
this.listenTo(view, 'change', () => {
const data = this.fetch();
- this.model.set(data);
+ this.model.setMultiple(data, {ui: true});
});
});
}
diff --git a/client/src/views/role/record/table.js b/client/src/views/role/record/table.js
index 997f8b0204..0ba5d8975f 100644
--- a/client/src/views/role/record/table.js
+++ b/client/src/views/role/record/table.js
@@ -115,6 +115,22 @@ class RoleRecordTableView extends View {
*/
acl
+ /**
+ * @private
+ * @type {({
+ * list: {
+ * level: string|false,
+ * name: string,
+ * action: string,
+ * levelList: string[]|null,
+ * }[],
+ * name: string,
+ * type: 'boolean'|'record',
+ * access: 'not-set'|'enabled'|'disabled',
+ * }|false)[]}
+ */
+ tableDataList
+
/**
* @private
* @type {
@@ -126,6 +142,7 @@ class RoleRecordTableView extends View {
* action: 'read'|'edit',
* value: 'yes'|'no',
* name: string,
+ * levelList: string[]|null,
* }[],
* }[],
* }[]
@@ -143,7 +160,7 @@ class RoleRecordTableView extends View {
data.fieldActionList = this.fieldActionList;
data.fieldLevelList = this.fieldLevelList;
- data.tableDataList = this.getTableDataList();
+ data.tableDataList = this.tableDataList;
data.fieldTableDataList = this.fieldTableDataList;
let hasFieldLevelData = false;
@@ -375,7 +392,7 @@ class RoleRecordTableView extends View {
const promises = [];
- this.getTableDataList().forEach(scopeItem => {
+ this.tableDataList.forEach(scopeItem => {
if (!scopeItem) {
return;
}
@@ -529,7 +546,7 @@ class RoleRecordTableView extends View {
if (actionItem.action === 'read') {
this.listenTo(this.formModel, `change:${scope}-${field}-read`, (m, value) => {
- this.controlFieldEditSelect(scope, field, value, true);
+ this.controlFieldEditSelect(scope, field, value);
});
}
});
@@ -538,7 +555,7 @@ class RoleRecordTableView extends View {
const readLevel = this.formModel.attributes[`${scope}-${field}-read`];
if (readLevel) {
- this.controlFieldEditSelect(scope, field, readLevel);
+ this.controlFieldEditSelect(scope, field, readLevel, true);
}
}
@@ -564,6 +581,7 @@ class RoleRecordTableView extends View {
}
this.setupScopeList();
+ this.tableDataList = this.getTableDataList();
this.setupFieldTableDataList();
}
@@ -679,6 +697,7 @@ class RoleRecordTableView extends View {
name: `${scope}-${field}-${action}`,
action: action,
value: scopeData[field][action] || 'yes',
+ levelList: [...this.fieldLevelList],
})
});
@@ -826,7 +845,7 @@ class RoleRecordTableView extends View {
const options = this.fieldLevelList
.filter(item => this.levelList.indexOf(item) >= this.levelList.indexOf(limitValue));
- if (!dontChange) {
+ if (!dontChange && this.hasFieldAction(scope, field, 'edit')) {
this.formModel.set(attribute, value);
}
@@ -861,7 +880,7 @@ class RoleRecordTableView extends View {
const options = this.getLevelList(scope, action)
.filter(item => this.levelList.indexOf(item) >= this.levelList.indexOf(limitValue));
- if (!dontChange) {
+ if (!dontChange && this.hasAction(scope, action)) {
setTimeout(() => this.formModel.set(attribute, value), 0);
}
@@ -874,6 +893,43 @@ class RoleRecordTableView extends View {
}
}
+ /**
+ * @private
+ * @param {string} scope
+ * @param {string} action
+ * @return {boolean}
+ */
+ hasAction(scope, action) {
+ if (this.tableDataList === false) {
+ return false;
+ }
+
+ return !!this.tableDataList
+ ?.find(it => it.name === scope)
+ ?.list
+ .find(it => it.action === action)
+ ?.levelList
+ ?.length;
+ }
+
+ /**
+ * @private
+ * @param {string} scope
+ * @param {string} field
+ * @param {string} action
+ * @return {boolean}
+ */
+ hasFieldAction(scope, field, action) {
+ return !!this.fieldTableDataList
+ ?.find(it => it.name === scope)
+ ?.list
+ ?.find(it => it.name === field)
+ ?.list
+ .find(it => it.action === action)
+ ?.levelList
+ ?.length;
+ }
+
/**
* @private
* @param {string} scope
@@ -908,11 +964,13 @@ class RoleRecordTableView extends View {
name: `${scope}-${field}-read`,
action: 'read',
value: 'no',
+ levelList: [...this.fieldLevelList],
},
{
name: `${scope}-${field}-edit`,
action: 'edit',
value: 'no',
+ levelList: [...this.fieldLevelList],
},
]
};
diff --git a/frontend/less/espo/elements/form.less b/frontend/less/espo/elements/form.less
index d086a0e455..ac368726e8 100644
--- a/frontend/less/espo/elements/form.less
+++ b/frontend/less/espo/elements/form.less
@@ -590,6 +590,9 @@ select.form-control.native-select {
appearance: base-select;
}
+ // Otherwise, the picker displayed in a wrong place.
+ display: inline-flex;
+
&:open {
border-color: var(--input-border-focus);
outline: 0;
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'));
+ }
}
diff --git a/tests/unit/Espo/Core/Field/Currency/CurrencyTest.php b/tests/unit/Espo/Core/Field/Currency/CurrencyTest.php
index 1651676dde..3bfe3d9b87 100644
--- a/tests/unit/Espo/Core/Field/Currency/CurrencyTest.php
+++ b/tests/unit/Espo/Core/Field/Currency/CurrencyTest.php
@@ -39,7 +39,7 @@ use InvalidArgumentException;
class CurrencyTest extends TestCase
{
- public function testValue()
+ public function testValue1()
{
$value = Currency::create(2.0, 'USD');
@@ -47,6 +47,14 @@ class CurrencyTest extends TestCase
$this->assertEquals('USD', $value->getCode());
}
+ public function testValue()
+ {
+ $value = Currency::create(0.5, 'USD');
+
+ $this->assertEquals('0.5', $value->getAmountAsString());
+ $this->assertEquals('USD', $value->getCode());
+ }
+
public function testAdd()
{
$value = (new Currency(2.0, 'USD'))->add(
@@ -67,7 +75,7 @@ class CurrencyTest extends TestCase
$this->assertEquals('USD', $value->getCode());
}
- public function testMultiply()
+ public function testMultiply1()
{
$value = (new Currency(2.0, 'USD'))->multiply(3.0);
@@ -75,7 +83,15 @@ class CurrencyTest extends TestCase
$this->assertEquals('USD', $value->getCode());
}
- public function testDivide()
+ public function testMultiply2()
+ {
+ $value = (new Currency(2.0, 'USD'))->multiply(0.5);
+
+ $this->assertEquals('1.00000000000000', $value->getAmountAsString());
+ $this->assertEquals('USD', $value->getCode());
+ }
+
+ public function testDivide1()
{
$value = (new Currency(6.0, 'USD'))->divide(3.0);
@@ -83,6 +99,14 @@ class CurrencyTest extends TestCase
$this->assertEquals('USD', $value->getCode());
}
+ public function testDivide2()
+ {
+ $value = (new Currency(6.0, 'USD'))->divide(0.5);
+
+ $this->assertEquals('12.00000000000000', $value->getAmount());
+ $this->assertEquals('USD', $value->getCode());
+ }
+
public function testRound1()
{
$value = (new Currency(2.306, 'USD'))->round(2);