Currency rates as entities (#3543)
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
<?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\Classes\Acl\CurrencyRecordRate;
|
||||||
|
|
||||||
|
use Espo\Core\Acl\AccessEntityCREDChecker;
|
||||||
|
use Espo\Core\Acl\DefaultAccessChecker;
|
||||||
|
use Espo\Core\Acl\ScopeData;
|
||||||
|
use Espo\Core\Acl\Table;
|
||||||
|
use Espo\Core\Acl\Traits\DefaultAccessCheckerDependency;
|
||||||
|
use Espo\Entities\CurrencyRecordRate;
|
||||||
|
use Espo\Entities\User;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements AccessEntityCREDChecker<CurrencyRecordRate>
|
||||||
|
*/
|
||||||
|
class AccessChecker implements AccessEntityCREDChecker
|
||||||
|
{
|
||||||
|
use DefaultAccessCheckerDependency;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private DefaultAccessChecker $defaultAccessChecker,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function checkCreate(User $user, ScopeData $data): bool
|
||||||
|
{
|
||||||
|
if ($data->getEdit() === Table::LEVEL_YES) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkDelete(User $user, ScopeData $data): bool
|
||||||
|
{
|
||||||
|
if ($data->getEdit() === Table::LEVEL_YES) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||||
|
{
|
||||||
|
return $this->checkCreate($user, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||||
|
{
|
||||||
|
return $this->checkDelete($user, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?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\Classes\AppParams;
|
||||||
|
|
||||||
|
use Espo\Core\Currency\ConfigDataProvider;
|
||||||
|
use Espo\Core\Utils\NumberUtil;
|
||||||
|
use Espo\Tools\App\AppParam;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
class CurrencyRates implements AppParam
|
||||||
|
{
|
||||||
|
private const int PRECISION = 6;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
private NumberUtil $numberUtil,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function get(): stdClass
|
||||||
|
{
|
||||||
|
$rates = $this->configDataProvider->getCurrencyRates()->toAssoc();
|
||||||
|
|
||||||
|
foreach ($rates as $code => $value) {
|
||||||
|
$rates[$code] = $this->numberUtil->format($value, self::PRECISION);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (object) $rates;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?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\Classes\FieldProcessing\CurrencyRecord;
|
||||||
|
|
||||||
|
use Espo\Core\Currency\ConfigDataProvider;
|
||||||
|
use Espo\Core\FieldProcessing\Loader;
|
||||||
|
use Espo\Core\FieldProcessing\Loader\Params;
|
||||||
|
use Espo\Entities\CurrencyRecord;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use ValueError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements Loader<CurrencyRecord>
|
||||||
|
*/
|
||||||
|
class IsBase implements Loader
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
public function process(Entity $entity, Params $params): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$code = $entity->getCode();
|
||||||
|
} catch (ValueError) {
|
||||||
|
$entity->setIsBase(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isBase = $code === $this->configDataProvider->getBaseCurrency();
|
||||||
|
|
||||||
|
$entity->setIsBase($isBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?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\Classes\FieldProcessing\CurrencyRecord;
|
||||||
|
|
||||||
|
use Espo\Core\FieldProcessing\Loader;
|
||||||
|
use Espo\Core\FieldProcessing\Loader\Params;
|
||||||
|
use Espo\Core\Utils\Language;
|
||||||
|
use Espo\Entities\CurrencyRecord;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use ValueError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements Loader<CurrencyRecord>
|
||||||
|
*/
|
||||||
|
class Label implements Loader
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Language $defaultLanguage
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Entity $entity, Params $params): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$code = $entity->getCode();
|
||||||
|
} catch (ValueError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->defaultLanguage->translateLabel($code, 'names', 'Currency');
|
||||||
|
|
||||||
|
$entity->setLabel($name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?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\Classes\FieldProcessing\CurrencyRecord;
|
||||||
|
|
||||||
|
use Espo\Core\Currency\ConfigDataProvider;
|
||||||
|
use Espo\Core\FieldProcessing\Loader;
|
||||||
|
use Espo\Core\FieldProcessing\Loader\Params;
|
||||||
|
use Espo\Entities\CurrencyRecord;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Tools\Currency\RateEntryProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements Loader<CurrencyRecord>
|
||||||
|
*/
|
||||||
|
class Rate implements Loader
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RateEntryProvider $rateEntryProvider,
|
||||||
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Entity $entity, Params $params): void
|
||||||
|
{
|
||||||
|
$rate = $entity->getCode() !== $this->configDataProvider->getBaseCurrency() ?
|
||||||
|
$this->rateEntryProvider->getCurrentRateEntry($entity)?->getRate() :
|
||||||
|
'1';
|
||||||
|
|
||||||
|
$entity->setRate($rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?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\Classes\FieldProcessing\CurrencyRecord;
|
||||||
|
|
||||||
|
use Espo\Core\Currency\ConfigDataProvider;
|
||||||
|
use Espo\Core\FieldProcessing\Loader;
|
||||||
|
use Espo\Core\FieldProcessing\Loader\Params;
|
||||||
|
use Espo\Entities\CurrencyRecord;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Tools\Currency\RateEntryProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements Loader<CurrencyRecord>
|
||||||
|
*/
|
||||||
|
class RateDate implements Loader
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RateEntryProvider $rateEntryProvider,
|
||||||
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Entity $entity, Params $params): void
|
||||||
|
{
|
||||||
|
$date = $entity->getCode() !== $this->configDataProvider->getBaseCurrency() ?
|
||||||
|
$this->rateEntryProvider->getCurrentRateEntry($entity)?->getDate() :
|
||||||
|
null;
|
||||||
|
|
||||||
|
$entity->setRateDate($date);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?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\Classes\FieldProcessing\CurrencyRecord;
|
||||||
|
|
||||||
|
use Espo\Core\FieldProcessing\Loader;
|
||||||
|
use Espo\Core\FieldProcessing\Loader\Params;
|
||||||
|
use Espo\Core\Utils\Metadata;
|
||||||
|
use Espo\Entities\CurrencyRecord;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use ValueError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements Loader<CurrencyRecord>
|
||||||
|
*/
|
||||||
|
class Symbol implements Loader
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Metadata $metadata,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Entity $entity, Params $params): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$code = $entity->getCode();
|
||||||
|
} catch (ValueError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$symbol = $this->metadata->get("app.currency.symbolMap.$code");
|
||||||
|
|
||||||
|
$entity->setSymbol($symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?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\Classes\FieldValidators\CurrencyRecordRate\Record;
|
||||||
|
|
||||||
|
use Espo\Core\Currency\ConfigDataProvider;
|
||||||
|
use Espo\Core\FieldValidation\Validator;
|
||||||
|
use Espo\Core\FieldValidation\Validator\Data;
|
||||||
|
use Espo\Core\FieldValidation\Validator\Failure;
|
||||||
|
use Espo\Entities\CurrencyRecordRate;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements Validator<CurrencyRecordRate>
|
||||||
|
*/
|
||||||
|
class NonBase implements Validator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||||
|
{
|
||||||
|
if ($entity->getRecord()->getCode() === $this->configDataProvider->getBaseCurrency()) {
|
||||||
|
return Failure::create();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?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\Classes\RecordHooks\CurrencyRecordRate;
|
||||||
|
|
||||||
|
use Espo\Core\Exceptions\Conflict;
|
||||||
|
use Espo\Core\Exceptions\Error;
|
||||||
|
use Espo\Core\Record\DeleteParams;
|
||||||
|
use Espo\Core\Record\Hook\DeleteHook;
|
||||||
|
use Espo\Entities\CurrencyRecordRate;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Tools\Currency\Exceptions\NotEnabled;
|
||||||
|
use Espo\Tools\Currency\RecordManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements DeleteHook<CurrencyRecordRate>
|
||||||
|
*/
|
||||||
|
class AfterDelete implements DeleteHook
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RecordManager $recordManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Entity $entity, DeleteParams $params): void
|
||||||
|
{
|
||||||
|
$code = $entity->getRecord()->getCode();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->recordManager->syncCodeToConfig($code);
|
||||||
|
} catch (NotEnabled $e) {
|
||||||
|
throw new Conflict($e->getMessage(), previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?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\Classes\RecordHooks\CurrencyRecordRate;
|
||||||
|
|
||||||
|
use Espo\Core\Exceptions\Conflict;
|
||||||
|
use Espo\Core\Record\Hook\SaveHook;
|
||||||
|
use Espo\Entities\CurrencyRecordRate;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Tools\Currency\Exceptions\NotEnabled;
|
||||||
|
use Espo\Tools\Currency\RecordManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements SaveHook<CurrencyRecordRate>
|
||||||
|
*/
|
||||||
|
class AfterSave implements SaveHook
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RecordManager $recordManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Entity $entity): void
|
||||||
|
{
|
||||||
|
$code = $entity->getRecord()->getCode();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->recordManager->syncCodeToConfig($code);
|
||||||
|
} catch (NotEnabled $e) {
|
||||||
|
throw new Conflict($e->getMessage(), previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?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\Classes\RecordHooks\CurrencyRecordRate;
|
||||||
|
|
||||||
|
use Espo\Core\Exceptions\Conflict;
|
||||||
|
use Espo\Core\Exceptions\Error\Body;
|
||||||
|
use Espo\Core\Record\Hook\SaveHook;
|
||||||
|
use Espo\Entities\CurrencyRecordRate;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements SaveHook<CurrencyRecordRate>
|
||||||
|
*/
|
||||||
|
class BeforeSaveValidation implements SaveHook
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Entity $entity): void
|
||||||
|
{
|
||||||
|
$this->validateDate($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Conflict
|
||||||
|
*/
|
||||||
|
private function validateDate(CurrencyRecordRate $entity): void
|
||||||
|
{
|
||||||
|
if (!$entity->isNew()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$recordId = $entity->getRecord()->getId();
|
||||||
|
$date = $entity->getDate();
|
||||||
|
|
||||||
|
$one = $this->entityManager
|
||||||
|
->getRDBRepositoryByClass(CurrencyRecordRate::class)
|
||||||
|
->where([
|
||||||
|
CurrencyRecordRate::ATTR_RECORD_ID => $recordId,
|
||||||
|
CurrencyRecordRate::FIELD_DATE => $date->toString(),
|
||||||
|
])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($one) {
|
||||||
|
throw Conflict::createWithBody(
|
||||||
|
'rateOnDateAlreadyExists',
|
||||||
|
Body::create()->withMessageTranslation('rateOnDateAlreadyExists', 'Currency')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?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\Classes\Select\CurrencyRecord\PrimaryFilters;
|
||||||
|
|
||||||
|
use Espo\Core\Select\Primary\Filter;
|
||||||
|
use Espo\Entities\CurrencyRecord;
|
||||||
|
use Espo\ORM\Query\SelectBuilder;
|
||||||
|
|
||||||
|
class Active implements Filter
|
||||||
|
{
|
||||||
|
public function apply(SelectBuilder $queryBuilder): void
|
||||||
|
{
|
||||||
|
$queryBuilder->where([
|
||||||
|
CurrencyRecord::FIELD_STATUS => CurrencyRecord::STATUS_ACTIVE,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?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\Controllers;
|
||||||
|
|
||||||
|
use Espo\Core\Api\Request;
|
||||||
|
use Espo\Core\Api\Response;
|
||||||
|
use Espo\Core\Controllers\Record;
|
||||||
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
class CurrencyRecord extends Record
|
||||||
|
{
|
||||||
|
public function postActionCreate(Request $request, Response $response): never
|
||||||
|
{
|
||||||
|
throw new Forbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putActionUpdate(Request $request, Response $response): never
|
||||||
|
{
|
||||||
|
throw new Forbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteActionDelete(Request $request, Response $response): never
|
||||||
|
{
|
||||||
|
throw new Forbidden();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?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\Controllers;
|
||||||
|
|
||||||
|
use Espo\Core\Controllers\RecordBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
class CurrencyRecordRate extends RecordBase
|
||||||
|
{}
|
||||||
@@ -32,6 +32,9 @@ namespace Espo\Core\Rebuild\Actions;
|
|||||||
use Espo\Core\Rebuild\RebuildAction;
|
use Espo\Core\Rebuild\RebuildAction;
|
||||||
use Espo\Core\Utils\Currency\DatabasePopulator;
|
use Espo\Core\Utils\Currency\DatabasePopulator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
class CurrencyRates implements RebuildAction
|
class CurrencyRates implements RebuildAction
|
||||||
{
|
{
|
||||||
public function __construct(private DatabasePopulator $databasePopulator) {}
|
public function __construct(private DatabasePopulator $databasePopulator) {}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?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\Core\Rebuild\Actions;
|
||||||
|
|
||||||
|
use Espo\Core\Rebuild\RebuildAction;
|
||||||
|
use Espo\Tools\Currency\RecordManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
class SyncCurrency implements RebuildAction
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RecordManager $recordManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(): void
|
||||||
|
{
|
||||||
|
$this->recordManager->sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,30 +33,28 @@ use Espo\Core\Currency\ConfigDataProvider;
|
|||||||
use Espo\Entities\Currency;
|
use Espo\Entities\Currency;
|
||||||
use Espo\ORM\EntityManager;
|
use Espo\ORM\EntityManager;
|
||||||
use Espo\ORM\Name\Attribute;
|
use Espo\ORM\Name\Attribute;
|
||||||
|
use Espo\Tools\Currency\RateEntryProvider;
|
||||||
|
use Espo\Tools\Currency\Exceptions\NotEnabled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates currency rates into database.
|
* Populates currency rates into database.
|
||||||
*/
|
*/
|
||||||
class DatabasePopulator
|
class DatabasePopulator
|
||||||
{
|
{
|
||||||
private const PRECISION = 5;
|
private const int PRECISION = 6;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EntityManager $entityManager,
|
private EntityManager $entityManager,
|
||||||
private ConfigDataProvider $configDataProvider,
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
private RateEntryProvider $rateEntryProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function process(): void
|
public function process(): void
|
||||||
{
|
{
|
||||||
$defaultCurrency = $this->configDataProvider->getDefaultCurrency();
|
$defaultCurrency = $this->configDataProvider->getDefaultCurrency();
|
||||||
$baseCurrency = $this->configDataProvider->getBaseCurrency();
|
$baseCurrency = $this->configDataProvider->getBaseCurrency();
|
||||||
$currencyRates = $this->configDataProvider->getCurrencyRates()->toAssoc();
|
|
||||||
|
|
||||||
if ($defaultCurrency !== $baseCurrency) {
|
$currencyRates = $this->prepareRates($defaultCurrency, $baseCurrency);
|
||||||
$currencyRates = $this->exchangeRates($baseCurrency, $defaultCurrency, $currencyRates);
|
|
||||||
}
|
|
||||||
|
|
||||||
$currencyRates[$defaultCurrency] = 1.00;
|
|
||||||
|
|
||||||
$this->entityManager->getTransactionManager()->start();
|
$this->entityManager->getTransactionManager()->start();
|
||||||
|
|
||||||
@@ -96,4 +94,28 @@ class DatabasePopulator
|
|||||||
|
|
||||||
return $exchangedRates;
|
return $exchangedRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, float>
|
||||||
|
*/
|
||||||
|
private function prepareRates(string $defaultCurrency, string $baseCurrency): array
|
||||||
|
{
|
||||||
|
$currencyRates = [];
|
||||||
|
|
||||||
|
foreach ($this->configDataProvider->getCurrencyList() as $itCode) {
|
||||||
|
try {
|
||||||
|
$currencyRates[$itCode] = (float) ($this->rateEntryProvider->getRate($itCode) ?? 1);
|
||||||
|
} catch (NotEnabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($defaultCurrency !== $baseCurrency) {
|
||||||
|
$currencyRates = $this->exchangeRates($baseCurrency, $defaultCurrency, $currencyRates);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currencyRates[$defaultCurrency] = 1.00;
|
||||||
|
|
||||||
|
return $currencyRates;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ class Currency implements FieldConverter
|
|||||||
{
|
{
|
||||||
$name = $fieldDefs->getName();
|
$name = $fieldDefs->getName();
|
||||||
|
|
||||||
$alias = $name . 'CurrencyRate';
|
$alias = $name . 'CurrencyRecordRate';
|
||||||
$leftJoins = [
|
$leftJoins = [
|
||||||
[
|
[
|
||||||
'Currency',
|
'Currency',
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?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\Entities;
|
||||||
|
|
||||||
|
use Espo\Core\Field\Date;
|
||||||
|
use Espo\Core\ORM\Entity;
|
||||||
|
|
||||||
|
class CurrencyRecord extends Entity
|
||||||
|
{
|
||||||
|
public const string ENTITY_TYPE = 'CurrencyRecord';
|
||||||
|
|
||||||
|
public const string FIELD_STATUS = 'status';
|
||||||
|
public const string FIELD_CODE = 'code';
|
||||||
|
|
||||||
|
public const string STATUS_ACTIVE = 'Active';
|
||||||
|
public const string STATUS_INACTIVE = 'Inactive';
|
||||||
|
|
||||||
|
public function getCode(): string
|
||||||
|
{
|
||||||
|
return $this->get(self::FIELD_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCode(string $code): self
|
||||||
|
{
|
||||||
|
return $this->set(self::FIELD_CODE, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): string
|
||||||
|
{
|
||||||
|
return $this->get(self::FIELD_STATUS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(string $status): self
|
||||||
|
{
|
||||||
|
return $this->set(self::FIELD_STATUS, $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLabel(?string $label): self
|
||||||
|
{
|
||||||
|
return $this->set('label', $label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSymbol(?string $label): self
|
||||||
|
{
|
||||||
|
return $this->set('symbol', $label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsBase(bool $isBase): self
|
||||||
|
{
|
||||||
|
return $this->set('isBase', $isBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ?numeric-string $rate
|
||||||
|
*/
|
||||||
|
public function setRate(?string $rate): self
|
||||||
|
{
|
||||||
|
return $this->set('rate', $rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRateDate(?Date $date): self
|
||||||
|
{
|
||||||
|
return $this->setValueObject('rateDate', $date);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?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\Entities;
|
||||||
|
|
||||||
|
use Espo\Core\Field\Date;
|
||||||
|
use Espo\Core\ORM\Entity;
|
||||||
|
use ValueError;
|
||||||
|
|
||||||
|
class CurrencyRecordRate extends Entity
|
||||||
|
{
|
||||||
|
public const string ENTITY_TYPE = 'CurrencyRecordRate';
|
||||||
|
|
||||||
|
public const string FIELD_DATE = 'date';
|
||||||
|
public const string FIELD_BASE_CODE = 'baseCode';
|
||||||
|
public const string FIELD_RATE = 'rate';
|
||||||
|
public const string FIELD_RECORD = 'record';
|
||||||
|
|
||||||
|
public const string ATTR_RECORD_ID = 'recordId';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return numeric-string
|
||||||
|
*/
|
||||||
|
public function getRate(): string
|
||||||
|
{
|
||||||
|
/** @var numeric-string */
|
||||||
|
return $this->get(self::FIELD_RATE) ?? '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param numeric-string $rate
|
||||||
|
*/
|
||||||
|
public function setRate(string $rate): self
|
||||||
|
{
|
||||||
|
return $this->set(self::FIELD_RATE, $rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBaseCode(string $code): self
|
||||||
|
{
|
||||||
|
return $this->set(self::FIELD_BASE_CODE, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRecord(CurrencyRecord $record): self
|
||||||
|
{
|
||||||
|
return $this->setRelatedLinkOrEntity(self::FIELD_RECORD, $record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDate(Date $date): self
|
||||||
|
{
|
||||||
|
return $this->setValueObject(self::DATE, $date);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecord(): CurrencyRecord
|
||||||
|
{
|
||||||
|
$record = $this->relations->getOne(self::FIELD_RECORD);
|
||||||
|
|
||||||
|
if (!$record instanceof CurrencyRecord) {
|
||||||
|
throw new ValueError("No record.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDate(): Date
|
||||||
|
{
|
||||||
|
$date = $this->getValueObject(self::FIELD_DATE);
|
||||||
|
|
||||||
|
if (!$date instanceof Date) {
|
||||||
|
throw new ValueError("No date.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?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\Hooks\CurrencyRecordRate;
|
||||||
|
|
||||||
|
use Espo\Core\Currency\ConfigDataProvider;
|
||||||
|
use Espo\Core\Hook\Hook\BeforeSave;
|
||||||
|
use Espo\Entities\CurrencyRecordRate;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\ORM\Repository\Option\SaveOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements BeforeSave<CurrencyRecordRate>
|
||||||
|
*/
|
||||||
|
class SetFields implements BeforeSave
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||||
|
{
|
||||||
|
if ($entity->isNew()) {
|
||||||
|
$baseCode = $this->configDataProvider->getBaseCurrency();
|
||||||
|
|
||||||
|
$entity->setBaseCode($baseCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,5 +164,8 @@
|
|||||||
"ZAR":"South African Rand",
|
"ZAR":"South African Rand",
|
||||||
"ZMW":"Zambian Kwacha",
|
"ZMW":"Zambian Kwacha",
|
||||||
"ZWL": "Zimbabwe Dollar"
|
"ZWL": "Zimbabwe Dollar"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"rateOnDateAlreadyExists": "A rate on this date already exists."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"code": "Code",
|
||||||
|
"status": "Status",
|
||||||
|
"label": "Label",
|
||||||
|
"symbol": "Symbol",
|
||||||
|
"isBase": "Is Base",
|
||||||
|
"rate": "Rate",
|
||||||
|
"rateDate": "Rate Date"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"rates": "Rates"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"status": {
|
||||||
|
"Active": "Active",
|
||||||
|
"Inactive": "Inactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CurrencyRecordRate": "Create Exchange Rate"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"record": "Currency",
|
||||||
|
"baseCode": "Base",
|
||||||
|
"date": "Date",
|
||||||
|
"rate": "Rate",
|
||||||
|
"number": "Number"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"record": "Currency"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,7 +68,9 @@
|
|||||||
"AppSecret": "App Secret",
|
"AppSecret": "App Secret",
|
||||||
"OAuthProvider": "OAuth Provider",
|
"OAuthProvider": "OAuth Provider",
|
||||||
"OAuthAccount": "OAuth Account",
|
"OAuthAccount": "OAuth Account",
|
||||||
"OpenApi": "OpenAPI"
|
"OpenApi": "OpenAPI",
|
||||||
|
"CurrencyRecord": "Currency Record",
|
||||||
|
"CurrencyRecordRate": "Currency Rate"
|
||||||
},
|
},
|
||||||
"scopeNamesPlural": {
|
"scopeNamesPlural": {
|
||||||
"Note": "Notes",
|
"Note": "Notes",
|
||||||
@@ -127,7 +129,9 @@
|
|||||||
"AppSecret": "App Secrets",
|
"AppSecret": "App Secrets",
|
||||||
"OAuthProvider": "OAuth Providers",
|
"OAuthProvider": "OAuth Providers",
|
||||||
"OAuthAccount": "OAuth Accounts",
|
"OAuthAccount": "OAuth Accounts",
|
||||||
"OpenApi": "OpenAPI"
|
"OpenApi": "OpenAPI",
|
||||||
|
"CurrencyRecord": "Currencies",
|
||||||
|
"CurrencyRecordRate": "Currency Rates"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Previous Page": "Previous Page",
|
"Previous Page": "Previous Page",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"rates": {
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isBase"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rateDate"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isBase"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rateDate"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"link": true,
|
||||||
|
"width": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "label",
|
||||||
|
"width": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symbol",
|
||||||
|
"width": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"width": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isBase",
|
||||||
|
"width": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rateDate",
|
||||||
|
"width": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate",
|
||||||
|
"align": "right"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"link": true,
|
||||||
|
"width": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "label",
|
||||||
|
"width": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"width": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isBase",
|
||||||
|
"width": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate",
|
||||||
|
"align": "right"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "record"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "baseCode"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "record"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "baseCode"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"date"
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "date",
|
||||||
|
"width": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "record",
|
||||||
|
"width": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "baseCode",
|
||||||
|
"width": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate",
|
||||||
|
"align": "right"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "date",
|
||||||
|
"width": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate",
|
||||||
|
"align": "right"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "date",
|
||||||
|
"width": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "record",
|
||||||
|
"width": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "baseCode",
|
||||||
|
"width": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate",
|
||||||
|
"align": "right"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -7,9 +7,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Currency Rates",
|
|
||||||
"rows": [
|
"rows": [
|
||||||
[{"name": "baseCurrency"}, {"name": "currencyRates"}]
|
[{"name": "baseCurrency"}, false]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"accessCheckerClassName": "Espo\\Classes\\Acl\\CurrencyRecordRate\\AccessChecker"
|
||||||
|
}
|
||||||
@@ -63,7 +63,9 @@
|
|||||||
"ImportError": "Import",
|
"ImportError": "Import",
|
||||||
"ImportEml": "Import",
|
"ImportEml": "Import",
|
||||||
"WorkingTimeRange": "WorkingTimeCalendar",
|
"WorkingTimeRange": "WorkingTimeCalendar",
|
||||||
"Stream": true
|
"Stream": true,
|
||||||
|
"CurrencyRecord": "boolean:Currency",
|
||||||
|
"CurrencyRecordRate": "Currency"
|
||||||
},
|
},
|
||||||
"fieldLevel": {
|
"fieldLevel": {
|
||||||
},
|
},
|
||||||
@@ -155,7 +157,11 @@
|
|||||||
"delete": "all"
|
"delete": "all"
|
||||||
},
|
},
|
||||||
"Stream": true,
|
"Stream": true,
|
||||||
"ImportEml": "Import"
|
"ImportEml": "Import",
|
||||||
|
"CurrencyRecordRate": {
|
||||||
|
"read": "yes",
|
||||||
|
"edit": "yes"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"valuePermissionList": [
|
"valuePermissionList": [
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
"iconClass": "fas fa-euro-sign",
|
"iconClass": "fas fa-euro-sign",
|
||||||
"description": "currency",
|
"description": "currency",
|
||||||
"recordView": "views/admin/currency"
|
"recordView": "views/admin/currency",
|
||||||
|
"view": "views/admin/currency-main"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "#Admin/notifications",
|
"url": "#Admin/notifications",
|
||||||
|
|||||||
@@ -7,5 +7,8 @@
|
|||||||
},
|
},
|
||||||
"addressCountryData": {
|
"addressCountryData": {
|
||||||
"className": "Espo\\Classes\\AppParams\\AddressCountryData"
|
"className": "Espo\\Classes\\AppParams\\AddressCountryData"
|
||||||
|
},
|
||||||
|
"currencyRates": {
|
||||||
|
"className": "Espo\\Classes\\AppParams\\CurrencyRates"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
},
|
},
|
||||||
"baselineRole": {
|
"baselineRole": {
|
||||||
"level": "admin"
|
"level": "admin"
|
||||||
|
},
|
||||||
|
"currencyRates": {
|
||||||
|
"readOnly": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
"actionClassNameList": [
|
"actionClassNameList": [
|
||||||
"Espo\\Core\\Rebuild\\Actions\\AddSystemUser",
|
"Espo\\Core\\Rebuild\\Actions\\AddSystemUser",
|
||||||
"Espo\\Core\\Rebuild\\Actions\\AddSystemData",
|
"Espo\\Core\\Rebuild\\Actions\\AddSystemData",
|
||||||
"Espo\\Core\\Rebuild\\Actions\\CurrencyRates",
|
|
||||||
"Espo\\Core\\Rebuild\\Actions\\ScheduledJobs",
|
"Espo\\Core\\Rebuild\\Actions\\ScheduledJobs",
|
||||||
"Espo\\Core\\Rebuild\\Actions\\ConfigMetadataCheck",
|
"Espo\\Core\\Rebuild\\Actions\\ConfigMetadataCheck",
|
||||||
"Espo\\Core\\Rebuild\\Actions\\GenerateInstanceId",
|
"Espo\\Core\\Rebuild\\Actions\\GenerateInstanceId",
|
||||||
"Espo\\Core\\Rebuild\\Actions\\SetIntegrationDefaults"
|
"Espo\\Core\\Rebuild\\Actions\\SetIntegrationDefaults",
|
||||||
|
"Espo\\Core\\Rebuild\\Actions\\SyncCurrency",
|
||||||
|
"Espo\\Core\\Rebuild\\Actions\\CurrencyRates"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"controller": "controllers/record",
|
||||||
|
"createDisabled": true,
|
||||||
|
"editDisabled": true,
|
||||||
|
"removeDisabled": true,
|
||||||
|
"nameAttribute": "code",
|
||||||
|
"defaultFilterData": {
|
||||||
|
"primary": "active"
|
||||||
|
},
|
||||||
|
"filterList": [
|
||||||
|
"active"
|
||||||
|
],
|
||||||
|
"viewSetupHandlers": {
|
||||||
|
"record/detail": [
|
||||||
|
"handlers/currency-record/record-detail"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"relationshipPanels": {
|
||||||
|
"rates": {
|
||||||
|
"layout": "listForRecord",
|
||||||
|
"createAttributeMap": {
|
||||||
|
"code": "recordName"
|
||||||
|
},
|
||||||
|
"view": "views/currency-record/record/panels/rates",
|
||||||
|
"unlinkDisabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"controller": "controllers/record",
|
||||||
|
"modelDefaultsPreparator": "handlers/currency-record-rate/default-preparator",
|
||||||
|
"acl": "acl/currency-record-rate",
|
||||||
|
"textFilterDisabled": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"code": {
|
||||||
|
"type": "varchar",
|
||||||
|
"maxLength": 3,
|
||||||
|
"required": true,
|
||||||
|
"readOnly": true,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "enum",
|
||||||
|
"options": [
|
||||||
|
"Active",
|
||||||
|
"Inactive"
|
||||||
|
],
|
||||||
|
"default": "Active",
|
||||||
|
"maxLength": 8,
|
||||||
|
"style": {
|
||||||
|
"Inactive": "info"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "varchar",
|
||||||
|
"notStorable": true,
|
||||||
|
"readOnly": true,
|
||||||
|
"orderDisabled": true,
|
||||||
|
"loaderClassName": "Espo\\Classes\\FieldProcessing\\CurrencyRecord\\Label"
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"type": "varchar",
|
||||||
|
"notStorable": true,
|
||||||
|
"readOnly": true,
|
||||||
|
"orderDisabled": true,
|
||||||
|
"loaderClassName": "Espo\\Classes\\FieldProcessing\\CurrencyRecord\\Symbol"
|
||||||
|
},
|
||||||
|
"rateDate": {
|
||||||
|
"type": "date",
|
||||||
|
"readOnly": true,
|
||||||
|
"notStorable": true,
|
||||||
|
"orderDisabled": true,
|
||||||
|
"loaderClassName": "Espo\\Classes\\FieldProcessing\\CurrencyRecord\\RateDate"
|
||||||
|
},
|
||||||
|
"rate": {
|
||||||
|
"type": "decimal",
|
||||||
|
"readOnly": true,
|
||||||
|
"notStorable": true,
|
||||||
|
"orderDisabled": true,
|
||||||
|
"decimalPlaces": 6,
|
||||||
|
"loaderClassName": "Espo\\Classes\\FieldProcessing\\CurrencyRecord\\Rate",
|
||||||
|
"view": "views/currency-record-rate/fields/rate"
|
||||||
|
},
|
||||||
|
"isBase": {
|
||||||
|
"type": "bool",
|
||||||
|
"readOnly": true,
|
||||||
|
"notStorable": true,
|
||||||
|
"orderDisabled": true,
|
||||||
|
"loaderClassName": "Espo\\Classes\\FieldProcessing\\CurrencyRecord\\IsBase"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"rates": {
|
||||||
|
"type": "hasMany",
|
||||||
|
"entity": "CurrencyRecordRate",
|
||||||
|
"foreign": "record",
|
||||||
|
"readOnly": true,
|
||||||
|
"orderBy": "date",
|
||||||
|
"order": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"codeDeleteId": {
|
||||||
|
"type": "unique",
|
||||||
|
"columns": [
|
||||||
|
"code",
|
||||||
|
"deleteId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deleteId": true,
|
||||||
|
"collection": {
|
||||||
|
"textFilterFields": [
|
||||||
|
"code"
|
||||||
|
],
|
||||||
|
"orderBy": "code",
|
||||||
|
"order": "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"record": {
|
||||||
|
"type": "link",
|
||||||
|
"required": true,
|
||||||
|
"readOnlyAfterCreate": true,
|
||||||
|
"validatorClassNameList": [
|
||||||
|
"Espo\\Classes\\FieldValidators\\CurrencyRecordRate\\Record\\NonBase"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"baseCode": {
|
||||||
|
"type": "varchar",
|
||||||
|
"readOnly": true,
|
||||||
|
"maxLength": 3
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "date",
|
||||||
|
"required": true,
|
||||||
|
"readOnlyAfterCreate": true,
|
||||||
|
"default": "javascript: return this.dateTime.getToday();"
|
||||||
|
},
|
||||||
|
"rate": {
|
||||||
|
"type": "decimal",
|
||||||
|
"decimalPlaces": 6,
|
||||||
|
"min": 0.0001,
|
||||||
|
"precision": 15,
|
||||||
|
"scale": 8,
|
||||||
|
"required": true,
|
||||||
|
"audited": true,
|
||||||
|
"view": "views/currency-record-rate/fields/rate"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "datetime",
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
|
"modifiedAt": {
|
||||||
|
"type": "datetime",
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
|
"createdBy": {
|
||||||
|
"type": "link",
|
||||||
|
"readOnly": true,
|
||||||
|
"view": "views/fields/user",
|
||||||
|
"fieldManagerParamList": []
|
||||||
|
},
|
||||||
|
"modifiedBy": {
|
||||||
|
"type": "link",
|
||||||
|
"readOnly": true,
|
||||||
|
"view": "views/fields/user",
|
||||||
|
"fieldManagerParamList": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"record": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"entity": "CurrencyRecord",
|
||||||
|
"foreignName": "code"
|
||||||
|
},
|
||||||
|
"createdBy": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"entity": "User"
|
||||||
|
},
|
||||||
|
"modifiedBy": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"entity": "User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"recordIdBaseCodeDate": {
|
||||||
|
"type": "unique",
|
||||||
|
"columns": [
|
||||||
|
"recordId",
|
||||||
|
"baseCode",
|
||||||
|
"date",
|
||||||
|
"deleteId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deleteId": true,
|
||||||
|
"collection": {
|
||||||
|
"orderBy": "date",
|
||||||
|
"order": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -191,8 +191,7 @@
|
|||||||
"view": "views/settings/fields/default-currency"
|
"view": "views/settings/fields/default-currency"
|
||||||
},
|
},
|
||||||
"currencyRates": {
|
"currencyRates": {
|
||||||
"type": "base",
|
"type": "base"
|
||||||
"view": "views/settings/fields/currency-rates"
|
|
||||||
},
|
},
|
||||||
"outboundEmailIsShared": {
|
"outboundEmailIsShared": {
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"baseCode": {
|
||||||
|
"visible": {
|
||||||
|
"conditionGroup": [
|
||||||
|
{
|
||||||
|
"type": "isNotEmpty",
|
||||||
|
"attribute": "id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"beforeCreateHookClassNameList": [
|
||||||
|
"Espo\\Classes\\RecordHooks\\CurrencyRecordRate\\BeforeSaveValidation"
|
||||||
|
],
|
||||||
|
"beforeUpdateHookClassNameList": [
|
||||||
|
"Espo\\Classes\\RecordHooks\\CurrencyRecordRate\\BeforeSaveValidation"
|
||||||
|
],
|
||||||
|
"afterCreateHookClassNameList": [
|
||||||
|
"Espo\\Classes\\RecordHooks\\CurrencyRecordRate\\AfterSave"
|
||||||
|
],
|
||||||
|
"afterUpdateHookClassNameList": [
|
||||||
|
"Espo\\Classes\\RecordHooks\\CurrencyRecordRate\\AfterSave"
|
||||||
|
],
|
||||||
|
"afterDeleteHookClassNameList": [
|
||||||
|
"Espo\\Classes\\RecordHooks\\CurrencyRecordRate\\AfterDelete"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"entity": true,
|
||||||
|
"tab": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"entity": true,
|
||||||
|
"preserveAuditLog": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"primaryFilterClassNameMap": {
|
||||||
|
"active": "Espo\\Classes\\Select\\CurrencyRecord\\PrimaryFilters\\Active"
|
||||||
|
},
|
||||||
|
"selectAttributesDependencyMap": {
|
||||||
|
"id": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"accessControlFilterResolverClassName": "Espo\\Core\\Select\\AccessControl\\FilterResolvers\\Boolean"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"selectAttributesDependencyMap": {
|
||||||
|
"id": [
|
||||||
|
"recordId",
|
||||||
|
"recordName",
|
||||||
|
"baseCode"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"accessControlFilterResolverClassName": "Espo\\Core\\Select\\AccessControl\\FilterResolvers\\Boolean"
|
||||||
|
}
|
||||||
@@ -35,7 +35,6 @@ use Espo\Entities\Email;
|
|||||||
use Espo\Entities\Settings;
|
use Espo\Entities\Settings;
|
||||||
use Espo\ORM\Entity;
|
use Espo\ORM\Entity;
|
||||||
use Espo\ORM\EntityManager;
|
use Espo\ORM\EntityManager;
|
||||||
|
|
||||||
use Espo\Core\Exceptions\BadRequest;
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
use Espo\Core\Exceptions\Error;
|
use Espo\Core\Exceptions\Error;
|
||||||
use Espo\Core\Exceptions\Forbidden;
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
@@ -50,10 +49,9 @@ use Espo\Core\Utils\Metadata;
|
|||||||
use Espo\Core\Utils\Config;
|
use Espo\Core\Utils\Config;
|
||||||
use Espo\Core\Utils\Config\ConfigWriter;
|
use Espo\Core\Utils\Config\ConfigWriter;
|
||||||
use Espo\Core\Utils\Config\Access;
|
use Espo\Core\Utils\Config\Access;
|
||||||
|
|
||||||
use Espo\Entities\Portal;
|
use Espo\Entities\Portal;
|
||||||
use Espo\Repositories\Portal as PortalRepository;
|
use Espo\Repositories\Portal as PortalRepository;
|
||||||
|
use Espo\Tools\Currency\RecordManager as CurrencyRecordManager;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
class SettingsService
|
class SettingsService
|
||||||
@@ -74,6 +72,8 @@ class SettingsService
|
|||||||
private Config\SystemConfig $systemConfig,
|
private Config\SystemConfig $systemConfig,
|
||||||
private EmailConfigDataProvider $emailConfigDataProvider,
|
private EmailConfigDataProvider $emailConfigDataProvider,
|
||||||
private Acl\Cache\Clearer $aclCacheClearer,
|
private Acl\Cache\Clearer $aclCacheClearer,
|
||||||
|
private CurrencyRecordManager $currencyRecordManager,
|
||||||
|
private CurrencyDatabasePopulator $currencyDatabasePopulator,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -243,8 +243,9 @@ class SettingsService
|
|||||||
$this->aclCacheClearer->clearForAllInternalUsers();
|
$this->aclCacheClearer->clearForAllInternalUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data->defaultCurrency) || isset($data->baseCurrency) || isset($data->currencyRates)) {
|
if (isset($data->baseCurrency) || isset($data->currencyList) || isset($data->defaultCurrency)) {
|
||||||
$this->populateDatabaseWithCurrencyRates();
|
$this->currencyRecordManager->sync();
|
||||||
|
$this->currencyDatabasePopulator->process();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,11 +341,6 @@ class SettingsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function populateDatabaseWithCurrencyRates(): void
|
|
||||||
{
|
|
||||||
$this->injectableFactory->create(CurrencyDatabasePopulator::class)->process();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function filterData(stdClass $data): void
|
private function filterData(stdClass $data): void
|
||||||
{
|
{
|
||||||
$user = $this->applicationState->getUser();
|
$user = $this->applicationState->getUser();
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?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\Tools\Currency\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class NotEnabled extends Exception
|
||||||
|
{}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<?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\Tools\Currency;
|
||||||
|
|
||||||
|
use Espo\Core\Currency\ConfigDataProvider;
|
||||||
|
use Espo\Core\Field\Date;
|
||||||
|
use Espo\Core\Utils\DateTime;
|
||||||
|
use Espo\Entities\CurrencyRecord;
|
||||||
|
use Espo\Entities\CurrencyRecordRate;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
use Espo\ORM\Query\Part\Order;
|
||||||
|
use Espo\Tools\Currency\Exceptions\NotEnabled;
|
||||||
|
use WeakMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 9.3.0
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class RateEntryProvider
|
||||||
|
{
|
||||||
|
/** @var WeakMap<CurrencyRecord, ?CurrencyRecordRate> */
|
||||||
|
private WeakMap $map;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
private EntityManager $entityManager,
|
||||||
|
private DateTime $dateTime,
|
||||||
|
) {
|
||||||
|
$this->map = new WeakMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentRateEntry(CurrencyRecord $record): ?CurrencyRecordRate
|
||||||
|
{
|
||||||
|
if (!$this->map->offsetExists($record)) {
|
||||||
|
$this->map[$record] = $this->entityManager
|
||||||
|
->getRDBRepositoryByClass(CurrencyRecordRate::class)
|
||||||
|
->where([
|
||||||
|
CurrencyRecordRate::ATTR_RECORD_ID => $record->getId(),
|
||||||
|
CurrencyRecordRate::FIELD_BASE_CODE => $this->configDataProvider->getBaseCurrency(),
|
||||||
|
CurrencyRecordRate::FIELD_DATE . '<=' => $this->dateTime->getToday()->toString(),
|
||||||
|
])
|
||||||
|
->order(CurrencyRecordRate::FIELD_DATE, Order::DESC)
|
||||||
|
->findOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->map[$record];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotEnabled
|
||||||
|
*/
|
||||||
|
public function prepareNew(string $code, Date $date): CurrencyRecordRate
|
||||||
|
{
|
||||||
|
$record = $this->getRecordByCode($code);
|
||||||
|
|
||||||
|
$entry = $this->entityManager->getRDBRepositoryByClass(CurrencyRecordRate::class)->getNew();
|
||||||
|
|
||||||
|
$entry
|
||||||
|
->setRecord($record)
|
||||||
|
->setDate($date);
|
||||||
|
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rate against the base currency by a record.
|
||||||
|
*
|
||||||
|
* @return ?numeric-string
|
||||||
|
*/
|
||||||
|
private function getRateByRecord(CurrencyRecord $record): ?string
|
||||||
|
{
|
||||||
|
$rateEntry = $this->entityManager
|
||||||
|
->getRDBRepositoryByClass(CurrencyRecordRate::class)
|
||||||
|
->where([
|
||||||
|
CurrencyRecordRate::ATTR_RECORD_ID => $record->getId(),
|
||||||
|
CurrencyRecordRate::FIELD_BASE_CODE => $this->configDataProvider->getBaseCurrency(),
|
||||||
|
CurrencyRecordRate::FIELD_DATE . '<=' => $this->dateTime->getToday()->toString(),
|
||||||
|
])
|
||||||
|
->order(CurrencyRecordRate::FIELD_DATE, Order::DESC)
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
return $rateEntry?->getRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rate against the base currency.
|
||||||
|
*
|
||||||
|
* @param string $code
|
||||||
|
* @return ?numeric-string
|
||||||
|
* @throws NotEnabled
|
||||||
|
*/
|
||||||
|
public function getRate(string $code): ?string
|
||||||
|
{
|
||||||
|
$record = $this->getRecordByCode($code);
|
||||||
|
|
||||||
|
return $this->getRateByRecord($record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotEnabled
|
||||||
|
*/
|
||||||
|
public function getRateEntryOnDate(string $code, Date $date): ?CurrencyRecordRate
|
||||||
|
{
|
||||||
|
$record = $this->getRecordByCode($code);
|
||||||
|
|
||||||
|
return $this->entityManager
|
||||||
|
->getRDBRepositoryByClass(CurrencyRecordRate::class)
|
||||||
|
->where([
|
||||||
|
CurrencyRecordRate::ATTR_RECORD_ID => $record->getId(),
|
||||||
|
CurrencyRecordRate::FIELD_BASE_CODE => $this->configDataProvider->getBaseCurrency(),
|
||||||
|
CurrencyRecordRate::FIELD_DATE => $date->toString(),
|
||||||
|
])
|
||||||
|
->order(CurrencyRecordRate::FIELD_DATE, Order::DESC)
|
||||||
|
->findOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotEnabled
|
||||||
|
*/
|
||||||
|
private function getRecordByCode(string $code): CurrencyRecord
|
||||||
|
{
|
||||||
|
$record = $this->entityManager
|
||||||
|
->getRDBRepositoryByClass(CurrencyRecord::class)
|
||||||
|
->where([
|
||||||
|
CurrencyRecord::FIELD_CODE => $code,
|
||||||
|
CurrencyRecord::FIELD_STATUS => CurrencyRecord::STATUS_ACTIVE,
|
||||||
|
])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$record) {
|
||||||
|
throw new NotEnabled("Currency $code is not enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,18 +35,23 @@ use Espo\Core\Currency\Rates;
|
|||||||
use Espo\Core\Exceptions\BadRequest;
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
use Espo\Core\Exceptions\Forbidden;
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
use Espo\Core\Acl;
|
use Espo\Core\Acl;
|
||||||
use Espo\Core\Utils\Config\ConfigWriter;
|
|
||||||
use Espo\Core\Utils\Currency\DatabasePopulator;
|
use Espo\Core\Utils\Currency\DatabasePopulator;
|
||||||
|
use Espo\Core\Utils\DateTime;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
class RateService
|
class RateService
|
||||||
{
|
{
|
||||||
private const SCOPE = 'Currency';
|
private const string SCOPE = 'Currency';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ConfigWriter $configWriter,
|
|
||||||
private Acl $acl,
|
private Acl $acl,
|
||||||
private DatabasePopulator $databasePopulator,
|
private DatabasePopulator $databasePopulator,
|
||||||
private ConfigDataProvider $configDataProvider
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
private RecordManager $recordManager,
|
||||||
|
private RateEntryProvider $rateEntryProvider,
|
||||||
|
private DateTime $dateTime,
|
||||||
|
private EntityManager $entityManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,13 +59,7 @@ class RateService
|
|||||||
*/
|
*/
|
||||||
public function get(): Rates
|
public function get(): Rates
|
||||||
{
|
{
|
||||||
if (!$this->acl->check(self::SCOPE)) {
|
$this->checkReadAccess();
|
||||||
throw new Forbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->acl->getLevel(self::SCOPE, Table::ACTION_READ) !== Table::LEVEL_YES) {
|
|
||||||
throw new Forbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
$rates = Rates::create($this->configDataProvider->getBaseCurrency());
|
$rates = Rates::create($this->configDataProvider->getBaseCurrency());
|
||||||
|
|
||||||
@@ -76,6 +75,62 @@ class RateService
|
|||||||
* @throws Forbidden
|
* @throws Forbidden
|
||||||
*/
|
*/
|
||||||
public function set(Rates $rates): void
|
public function set(Rates $rates): void
|
||||||
|
{
|
||||||
|
$this->checkEditAccess();
|
||||||
|
|
||||||
|
$codeList = $this->configDataProvider->getCurrencyList();
|
||||||
|
$base = $this->configDataProvider->getBaseCurrency();
|
||||||
|
|
||||||
|
foreach ($rates->toAssoc() as $code => $value) {
|
||||||
|
if ($value < 0) {
|
||||||
|
throw new BadRequest("Bad value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($code, $codeList) || $code === $base) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->writeOne($code, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->recordManager->syncToConfig();
|
||||||
|
$this->databasePopulator->process();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeOne(string $code, float $value): void
|
||||||
|
{
|
||||||
|
$date = $this->dateTime->getToday();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rateEntry = $this->rateEntryProvider->getRateEntryOnDate($code, $date) ??
|
||||||
|
$this->rateEntryProvider->prepareNew($code, $date);
|
||||||
|
} catch (Exceptions\NotEnabled $e) {
|
||||||
|
throw new RuntimeException($e->getMessage(), previous: $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rateEntry->setRate((string) $value);
|
||||||
|
|
||||||
|
$this->entityManager->saveEntity($rateEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function checkReadAccess(): void
|
||||||
|
{
|
||||||
|
if (!$this->acl->check(self::SCOPE)) {
|
||||||
|
throw new Forbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->acl->getLevel(self::SCOPE, Table::ACTION_READ) !== Table::LEVEL_YES) {
|
||||||
|
throw new Forbidden();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function checkEditAccess(): void
|
||||||
{
|
{
|
||||||
if (!$this->acl->check(self::SCOPE)) {
|
if (!$this->acl->check(self::SCOPE)) {
|
||||||
throw new Forbidden();
|
throw new Forbidden();
|
||||||
@@ -84,39 +139,5 @@ class RateService
|
|||||||
if ($this->acl->getLevel(self::SCOPE, Table::ACTION_EDIT) !== Table::LEVEL_YES) {
|
if ($this->acl->getLevel(self::SCOPE, Table::ACTION_EDIT) !== Table::LEVEL_YES) {
|
||||||
throw new Forbidden();
|
throw new Forbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
$currencyList = $this->configDataProvider->getCurrencyList();
|
|
||||||
$baseCurrency = $this->configDataProvider->getBaseCurrency();
|
|
||||||
|
|
||||||
$set = [];
|
|
||||||
|
|
||||||
foreach ($rates->toAssoc() as $key => $value) {
|
|
||||||
if ($value < 0) {
|
|
||||||
throw new BadRequest("Bad value.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!in_array($key, $currencyList)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($key === $baseCurrency) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$set[$key] = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($currencyList as $currency) {
|
|
||||||
if ($currency === $baseCurrency) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$set[$currency] ??= $this->configDataProvider->getCurrencyRate($currency);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->configWriter->set('currencyRates', $set);
|
|
||||||
$this->configWriter->save();
|
|
||||||
|
|
||||||
$this->databasePopulator->process();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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\Tools\Currency;
|
||||||
|
|
||||||
|
use Espo\Core\Currency\ConfigDataProvider;
|
||||||
|
use Espo\Core\Utils\Config\ConfigWriter;
|
||||||
|
use Espo\Entities\CurrencyRecord;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
use Espo\ORM\Query\UpdateBuilder;
|
||||||
|
use Espo\Tools\Currency\Exceptions\NotEnabled;
|
||||||
|
|
||||||
|
class RecordManager
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
private EntityManager $entityManager,
|
||||||
|
private ConfigWriter $configWriter,
|
||||||
|
private RateEntryProvider $rateEntryProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function sync(): void
|
||||||
|
{
|
||||||
|
$this->entityManager->getTransactionManager()->run(function () {
|
||||||
|
$this->syncInTransaction();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->syncToConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncInTransaction(): void
|
||||||
|
{
|
||||||
|
$this->lock();
|
||||||
|
|
||||||
|
foreach ($this->configDataProvider->getCurrencyList() as $code) {
|
||||||
|
$this->syncCode($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->deactivateNotListed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncCode(string $code): void
|
||||||
|
{
|
||||||
|
$record = $this->entityManager
|
||||||
|
->getRDBRepositoryByClass(CurrencyRecord::class)
|
||||||
|
->where([CurrencyRecord::FIELD_CODE => $code])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$record) {
|
||||||
|
$record = $this->entityManager->getRDBRepositoryByClass(CurrencyRecord::class)->getNew();
|
||||||
|
|
||||||
|
$record->setCode($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
$record->setStatus(CurrencyRecord::STATUS_ACTIVE);
|
||||||
|
|
||||||
|
$this->entityManager->saveEntity($record);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deactivateNotListed(): void
|
||||||
|
{
|
||||||
|
$list = $this->configDataProvider->getCurrencyList();
|
||||||
|
|
||||||
|
$updateQuery = UpdateBuilder::create()
|
||||||
|
->in(CurrencyRecord::ENTITY_TYPE)
|
||||||
|
->set([
|
||||||
|
CurrencyRecord::FIELD_STATUS => CurrencyRecord::STATUS_INACTIVE,
|
||||||
|
])
|
||||||
|
->where([
|
||||||
|
CurrencyRecord::FIELD_CODE . '!=' => $list,
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$this->entityManager->getQueryExecutor()->execute($updateQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function lock(): void
|
||||||
|
{
|
||||||
|
$this->entityManager
|
||||||
|
->getRDBRepositoryByClass(CurrencyRecord::class)
|
||||||
|
->forUpdate()
|
||||||
|
->sth()
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncToConfig(): void
|
||||||
|
{
|
||||||
|
$rates = [];
|
||||||
|
|
||||||
|
foreach ($this->configDataProvider->getCurrencyList() as $code) {
|
||||||
|
try {
|
||||||
|
$rate = $this->rateEntryProvider->getRate($code) ?? '1.0';
|
||||||
|
} catch (Exceptions\NotEnabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rates[$code] = (float) $rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->configWriter->set('currencyRates', $rates);
|
||||||
|
$this->configWriter->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotEnabled
|
||||||
|
*/
|
||||||
|
public function syncCodeToConfig(string $code): void
|
||||||
|
{
|
||||||
|
$rates = $this->configDataProvider->getCurrencyRates()->toAssoc();
|
||||||
|
|
||||||
|
$rate = $this->rateEntryProvider->getRate($code) ?? '1.0';
|
||||||
|
|
||||||
|
$rates[$code] = (float) $rate;
|
||||||
|
|
||||||
|
$this->configWriter->set('currencyRates', $rates);
|
||||||
|
$this->configWriter->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
data-currency="{{@key}}"
|
data-currency="{{@key}}"
|
||||||
value="{{./this}}"
|
value="{{./this}}"
|
||||||
style="text-align: right;"
|
style="text-align: right;"
|
||||||
|
readonly="readonly"
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="input-group-addon radius-right" style="width: 22%">{{../baseCurrency}}</span>
|
<span class="input-group-addon radius-right" style="width: 22%">{{../baseCurrency}}</span>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
import Acl from 'acl';
|
||||||
|
|
||||||
|
export default class extends Acl {
|
||||||
|
|
||||||
|
checkScope(data, action, precise, entityAccessData) {
|
||||||
|
if (action === 'create' || action === 'delete') {
|
||||||
|
action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.checkScope(data, action, precise, entityAccessData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import SettingsEditView from 'views/settings/edit';
|
|||||||
import AdminIndexView from 'views/admin/index';
|
import AdminIndexView from 'views/admin/index';
|
||||||
import {inject} from 'di';
|
import {inject} from 'di';
|
||||||
import Language from 'language';
|
import Language from 'language';
|
||||||
|
import EditView from 'views/edit';
|
||||||
|
|
||||||
class AdminController extends Controller {
|
class AdminController extends Controller {
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ class AdminController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
actionPage(options) {
|
async actionPage(options) {
|
||||||
const page = options.page;
|
const page = options.page;
|
||||||
|
|
||||||
if (options.options) {
|
if (options.options) {
|
||||||
@@ -81,7 +82,7 @@ class AdminController extends Controller {
|
|||||||
throw new Espo.Exceptions.NotFound();
|
throw new Espo.Exceptions.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defs.view) {
|
if (defs.view && !defs.recordView) {
|
||||||
this.main(defs.view, options);
|
this.main(defs.view, options);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -93,23 +94,32 @@ class AdminController extends Controller {
|
|||||||
|
|
||||||
const model = this.getSettingsModel();
|
const model = this.getSettingsModel();
|
||||||
|
|
||||||
model.fetch().then(() => {
|
await model.fetch();
|
||||||
model.id = '1';
|
|
||||||
|
|
||||||
const editView = new SettingsEditView({
|
model.id = '1';
|
||||||
model: model,
|
|
||||||
headerTemplate: 'admin/settings/headers/page',
|
|
||||||
recordView: defs.recordView,
|
|
||||||
page: page,
|
|
||||||
label: defs.label,
|
|
||||||
optionsToPass: [
|
|
||||||
'page',
|
|
||||||
'label',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.main(editView);
|
const view = defs.view ?? 'views/settings/edit';
|
||||||
|
|
||||||
|
const ViewClass = await Espo.loader.requirePromise(view);
|
||||||
|
|
||||||
|
if (!EditView.isPrototypeOf(ViewClass)) {
|
||||||
|
throw new Error("View should inherit views/edit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const editView = new ViewClass({
|
||||||
|
model: model,
|
||||||
|
headerTemplate: 'admin/settings/headers/page',
|
||||||
|
recordView: defs.recordView,
|
||||||
|
page: page,
|
||||||
|
label: defs.label,
|
||||||
|
optionsToPass: [
|
||||||
|
'page',
|
||||||
|
'label',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.main(editView);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
import DefaultsPreparator from 'handlers/model/defaults-preparator';
|
||||||
|
import {inject} from 'di';
|
||||||
|
import Settings from 'models/settings';
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
export default class extends DefaultsPreparator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {Settings}
|
||||||
|
*/
|
||||||
|
@inject(Settings)
|
||||||
|
config
|
||||||
|
|
||||||
|
async prepare(model) {
|
||||||
|
return {
|
||||||
|
baseCode: this.config.get('baseCurrency'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
export default class CurrencyRecordRecordDetailHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('views/record/detail').default} view
|
||||||
|
*/
|
||||||
|
constructor(view) {
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
process() {
|
||||||
|
const model = this.view.model;
|
||||||
|
|
||||||
|
this.view.listenTo(model, 'after:relate:rates after:unrelate:rates after:related-change:rates', () => {
|
||||||
|
model.fetch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
import EditView from 'views/edit';
|
||||||
|
|
||||||
|
export default class CurrencyMainView extends EditView {
|
||||||
|
|
||||||
|
scope = 'Settings'
|
||||||
|
|
||||||
|
getHeader() {
|
||||||
|
return this.buildHeaderHtml([
|
||||||
|
(() => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = '#Admin';
|
||||||
|
a.text = this.translate('Administration');
|
||||||
|
|
||||||
|
return a;
|
||||||
|
})(),
|
||||||
|
(() => {
|
||||||
|
return this.options.label;
|
||||||
|
})(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
************************************************************************/
|
************************************************************************/
|
||||||
|
|
||||||
import SettingsEditRecordView from 'views/settings/record/edit';
|
import SettingsEditRecordView from 'views/settings/record/edit';
|
||||||
|
import EditView from 'views/edit';
|
||||||
|
|
||||||
export default class extends SettingsEditRecordView {
|
export default class extends SettingsEditRecordView {
|
||||||
|
|
||||||
@@ -65,6 +66,22 @@ export default class extends SettingsEditRecordView {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.controlCurrencyRatesVisibility();
|
this.controlCurrencyRatesVisibility();
|
||||||
|
|
||||||
|
this.whenReady().then(() => {
|
||||||
|
const view = /** @type {EditView} view */
|
||||||
|
this.getParentView();
|
||||||
|
|
||||||
|
if (!view instanceof EditView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.addMenuItem('buttons', {
|
||||||
|
name: 'currencyRecords',
|
||||||
|
link: '#CurrencyRecord',
|
||||||
|
labelTranslation: 'Settings.labels.Currency Rates',
|
||||||
|
iconClass: 'fas fa-euro-sign',
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
controlCurrencyRatesVisibility() {
|
controlCurrencyRatesVisibility() {
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
import DecimalFieldView from 'views/fields/decimal';
|
||||||
|
|
||||||
|
export default class CurrencyRecordRateRateFieldView extends DecimalFieldView {
|
||||||
|
|
||||||
|
// language=Handlebars
|
||||||
|
listTemplateContent = `
|
||||||
|
{{#if isNotEmpty~}}
|
||||||
|
<span class="text-soft">{{targetCode}} = </span>
|
||||||
|
<span class="numeric-text">{{value}}</span>
|
||||||
|
<span class="text-soft">{{baseCode}}</span>
|
||||||
|
{{~/if~}}
|
||||||
|
`
|
||||||
|
|
||||||
|
// language=Handlebars
|
||||||
|
detailTemplateContent = `
|
||||||
|
{{~#if isNotEmpty~}}
|
||||||
|
<span class="text-soft">{{targetCode}} = </span>
|
||||||
|
<span class="numeric-text">{{value}}</span>
|
||||||
|
<span class="text-soft">{{baseCode}}</span>
|
||||||
|
{{~else~}}
|
||||||
|
{{~#if valueIsSet~}}
|
||||||
|
<span class="none-value">{{translate 'None'}}</span>
|
||||||
|
{{~else~}}<span class="loading-value"></span>
|
||||||
|
{{~/if}}
|
||||||
|
{{~/if~}}
|
||||||
|
`
|
||||||
|
|
||||||
|
// language=Handlebars
|
||||||
|
editTemplateContent = `
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon radius-left" style="width: 24%">1 {{targetCode}} = </span>
|
||||||
|
<span class="input-group-item">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="main-element form-control numeric-text"
|
||||||
|
data-name="{{name}}"
|
||||||
|
value="{{value}}"
|
||||||
|
autocomplete="espo-{{name}}"
|
||||||
|
pattern="[\\-]?[0-9]*"
|
||||||
|
style="text-align: end;"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span class="input-group-addon radius-right" style="width: 21%">{{baseCode}}</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
getAttributeList() {
|
||||||
|
return [
|
||||||
|
...super.getAttributeList(),
|
||||||
|
'baseCode',
|
||||||
|
'recordName',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let baseCode = this.model.attributes.baseCode;
|
||||||
|
let targetCode = this.model.attributes.recordName;
|
||||||
|
|
||||||
|
if (this.model.entityType === 'CurrencyRecord') {
|
||||||
|
baseCode = this.getConfig().get('baseCurrency');
|
||||||
|
targetCode = this.model.attributes.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...super.data(),
|
||||||
|
baseCode,
|
||||||
|
targetCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
import RelationshipPanelView from 'views/record/panels/relationship';
|
||||||
|
|
||||||
|
export default class RatesPanelView extends RelationshipPanelView {
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
if (this.model.attributes.code === this.getConfig().get('baseCurrency')) {
|
||||||
|
this.defs.createDisabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,23 +28,33 @@
|
|||||||
|
|
||||||
import BaseFieldView from 'views/fields/base';
|
import BaseFieldView from 'views/fields/base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not used.
|
||||||
|
* @todo Remove.
|
||||||
|
*/
|
||||||
export default class extends BaseFieldView {
|
export default class extends BaseFieldView {
|
||||||
|
|
||||||
editTemplate = 'settings/fields/currency-rates/edit'
|
editTemplate = 'settings/fields/currency-rates/edit'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
baseCode
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
const baseCurrency = this.model.get('baseCurrency');
|
const baseCode = this.baseCode;
|
||||||
const currencyRates = this.model.get('currencyRates') || {};
|
const currencyRates = this.model.get('currencyRates') || {};
|
||||||
|
|
||||||
const rateValues = {};
|
const rateValues = {};
|
||||||
|
|
||||||
(this.model.get('currencyList') || []).forEach(currency => {
|
(this.model.get('currencyList') || []).forEach(currency => {
|
||||||
if (currency !== baseCurrency) {
|
if (currency !== baseCode) {
|
||||||
rateValues[currency] = currencyRates[currency];
|
rateValues[currency] = currencyRates[currency];
|
||||||
|
|
||||||
if (!rateValues[currency]) {
|
if (!rateValues[currency]) {
|
||||||
if (currencyRates[baseCurrency]) {
|
if (currencyRates[baseCode]) {
|
||||||
rateValues[currency] = Math.round(1 / currencyRates[baseCurrency] * 1000) / 1000;
|
rateValues[currency] = Math.round(1 / currencyRates[baseCode] * 1000) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rateValues[currency]) {
|
if (!rateValues[currency]) {
|
||||||
@@ -56,36 +66,23 @@ export default class extends BaseFieldView {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
rateValues: rateValues,
|
rateValues: rateValues,
|
||||||
baseCurrency: baseCurrency,
|
baseCurrency: baseCode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch() {
|
setup() {
|
||||||
const data = {};
|
const sync = () => {
|
||||||
const currencyRates = {};
|
this.baseCode = this.model.get('baseCurrency');
|
||||||
|
};
|
||||||
|
|
||||||
const baseCurrency = this.model.get('baseCurrency');
|
sync();
|
||||||
|
|
||||||
const currencyList = this.model.get('currencyList') || [];
|
this.listenTo(this.model, 'sync', () => {
|
||||||
|
sync();
|
||||||
currencyList.forEach(currency => {
|
|
||||||
if (currency !== baseCurrency) {
|
|
||||||
const value = this.$el.find(`input[data-currency="${currency}"]`).val() || '1';
|
|
||||||
|
|
||||||
currencyRates[currency] = parseFloat(value);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
delete currencyRates[baseCurrency];
|
fetch() {
|
||||||
|
return {};
|
||||||
for (const c in currencyRates) {
|
|
||||||
if (!~currencyList.indexOf(c)) {
|
|
||||||
delete currencyRates[c];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data[this.name] = currencyRates;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,11 +80,11 @@
|
|||||||
"description": "A 'create' access level."
|
"description": "A 'create' access level."
|
||||||
},
|
},
|
||||||
"read": {
|
"read": {
|
||||||
"$ref": "#/definitions/levels",
|
"$ref": "#/definitions/levelsAll",
|
||||||
"description": "A 'read' access level."
|
"description": "A 'read' access level."
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"$ref": "#/definitions/levels",
|
"$ref": "#/definitions/levelsAll",
|
||||||
"description": "An 'edit' access level."
|
"description": "An 'edit' access level."
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
},
|
},
|
||||||
"view": {
|
"view": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A view. Not compatible with `recordView`."
|
"description": "A view. Compatible with `recordView` as of v9.3."
|
||||||
},
|
},
|
||||||
"tabQuickSearch": {
|
"tabQuickSearch": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|||||||
@@ -37,15 +37,17 @@ use Espo\Core\Currency\Rates;
|
|||||||
use Espo\Core\Field\Currency;
|
use Espo\Core\Field\Currency;
|
||||||
use Espo\Core\InjectableFactory;
|
use Espo\Core\InjectableFactory;
|
||||||
use Espo\Core\Utils\Config\ConfigWriter;
|
use Espo\Core\Utils\Config\ConfigWriter;
|
||||||
|
use Espo\Tools\Currency\RecordManager;
|
||||||
|
use tests\integration\Core\BaseTestCase;
|
||||||
|
|
||||||
class CurrencyTest extends \tests\integration\Core\BaseTestCase
|
class CurrencyTest extends BaseTestCase
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @noinspection PhpUnhandledExceptionInspection
|
||||||
|
*/
|
||||||
public function testSetCurrencyRates(): void
|
public function testSetCurrencyRates(): void
|
||||||
{
|
{
|
||||||
$app = $this->createApplication();
|
$factory = $this->getInjectableFactory();
|
||||||
|
|
||||||
/** @var InjectableFactory $factory */
|
|
||||||
$factory = $app->getContainer()->get('injectableFactory');
|
|
||||||
|
|
||||||
$configWriter = $factory->create(ConfigWriter::class);
|
$configWriter = $factory->create(ConfigWriter::class);
|
||||||
|
|
||||||
@@ -55,8 +57,11 @@ class CurrencyTest extends \tests\integration\Core\BaseTestCase
|
|||||||
$configWriter->set('currencyRates', [
|
$configWriter->set('currencyRates', [
|
||||||
'EUR' => 1.2,
|
'EUR' => 1.2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$configWriter->save();
|
$configWriter->save();
|
||||||
|
|
||||||
|
$this->getInjectableFactory()->create(RecordManager::class)->sync();
|
||||||
|
|
||||||
$service = $factory->create(RateService::class);
|
$service = $factory->create(RateService::class);
|
||||||
|
|
||||||
$rates = Rates::fromAssoc(['EUR' => 1.3], '___');
|
$rates = Rates::fromAssoc(['EUR' => 1.3], '___');
|
||||||
|
|||||||
Reference in New Issue
Block a user