Use BigDecimal for Currency exchange rate.

This commit is contained in:
Jan Böhmer 2020-05-20 20:57:16 +02:00
parent 08267b88b0
commit db4d7dc5fc
9 changed files with 135 additions and 20 deletions

View file

@ -43,6 +43,7 @@ declare(strict_types=1);
namespace App\Command; namespace App\Command;
use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Currency;
use Brick\Math\BigDecimal;
use function count; use function count;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Exchanger\Exception\Exception; use Exchanger\Exception\Exception;
@ -122,10 +123,11 @@ class UpdateExchangeRatesCommand extends Command
//Iterate over each candidate and update exchange rate //Iterate over each candidate and update exchange rate
foreach ($candidates as $currency) { foreach ($candidates as $currency) {
/** @var Currency $currency */
try { try {
$rate = $swap->latest($currency->getIsoCode().'/'.$this->base_current); $rate = $swap->latest($currency->getIsoCode().'/'.$this->base_current);
$currency->setExchangeRate((string) $rate->getValue()); $currency->setExchangeRate(BigDecimal::of($rate->getValue()));
$io->note(sprintf('Set exchange rate of %s to %f', $currency->getIsoCode(), $currency->getExchangeRate())); $io->note(sprintf('Set exchange rate of %s to %f', $currency->getIsoCode(), $currency->getExchangeRate()->toFloat()));
$this->em->persist($currency); $this->em->persist($currency);
++$success_counter; ++$success_counter;

View file

@ -153,7 +153,7 @@ class Supplier extends AbstractCompany
/** /**
* Sets the shipping costs for an order with this supplier. * Sets the shipping costs for an order with this supplier.
* *
* @param string|null $shipping_costs a BigDecimal with the shipping costs * @param BigDecimal|null $shipping_costs a BigDecimal with the shipping costs
* *
* @return Supplier * @return Supplier
*/ */

View file

@ -45,6 +45,9 @@ namespace App\Entity\PriceInformations;
use App\Entity\Attachments\CurrencyAttachment; use App\Entity\Attachments\CurrencyAttachment;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parameters\CurrencyParameter; use App\Entity\Parameters\CurrencyParameter;
use App\Validator\Constraints\BigDecimal\BigDecimalPositive;
use Brick\Math\BigDecimal;
use Brick\Math\RoundingMode;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
@ -63,10 +66,10 @@ class Currency extends AbstractStructuralDBElement
public const PRICE_SCALE = 5; public const PRICE_SCALE = 5;
/** /**
* @var string|null The exchange rate between this currency and the base currency * @var BigDecimal|null The exchange rate between this currency and the base currency
* (how many base units the current currency is worth) * (how many base units the current currency is worth)
* @ORM\Column(type="decimal", precision=11, scale=5, nullable=true) * @ORM\Column(type="big_decimal", precision=11, scale=5, nullable=true)
* @Assert\Positive() * @BigDecimalPositive()
*/ */
protected $exchange_rate; protected $exchange_rate;
@ -148,22 +151,22 @@ class Currency extends AbstractStructuralDBElement
/** /**
* Returns the inverse exchange rate (how many of the current currency the base unit is worth). * Returns the inverse exchange rate (how many of the current currency the base unit is worth).
*/ */
public function getInverseExchangeRate(): ?string public function getInverseExchangeRate(): ?BigDecimal
{ {
$tmp = $this->getExchangeRate(); $tmp = $this->getExchangeRate();
if (null === $tmp || '0' === $tmp) { if (null === $tmp || $tmp->isZero()) {
return null; return null;
} }
return bcdiv('1', $tmp, static::PRICE_SCALE); return BigDecimal::one()->dividedBy($tmp, $tmp->getScale(), RoundingMode::HALF_UP);
} }
/** /**
* Returns The exchange rate between this currency and the base currency * Returns The exchange rate between this currency and the base currency
* (how many base units the current currency is worth). * (how many base units the current currency is worth).
*/ */
public function getExchangeRate(): ?string public function getExchangeRate(): ?BigDecimal
{ {
return $this->exchange_rate; return $this->exchange_rate;
} }
@ -171,12 +174,12 @@ class Currency extends AbstractStructuralDBElement
/** /**
* Sets the exchange rate of the currency. * Sets the exchange rate of the currency.
* *
* @param string|null $exchange_rate The new exchange rate of the currency. * @param BigDecimal|null $exchange_rate The new exchange rate of the currency.
* Set to null, if the exchange rate is unknown. * Set to null, if the exchange rate is unknown.
* *
* @return Currency * @return Currency
*/ */
public function setExchangeRate(?string $exchange_rate): self public function setExchangeRate(?BigDecimal $exchange_rate): self
{ {
$this->exchange_rate = $exchange_rate; $this->exchange_rate = $exchange_rate;

View file

@ -43,6 +43,7 @@ declare(strict_types=1);
namespace App\Form\AdminPages; namespace App\Form\AdminPages;
use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractNamedDBElement;
use App\Form\Type\BigDecimalMoneyType;
use Symfony\Component\Form\Extension\Core\Type\CurrencyType; use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@ -74,7 +75,7 @@ class CurrencyAdminForm extends BaseEntityAdminForm
'disabled' => ! $this->security->isGranted($is_new ? 'create' : 'edit', $entity), 'disabled' => ! $this->security->isGranted($is_new ? 'create' : 'edit', $entity),
]); ]);
$builder->add('exchange_rate', MoneyType::class, [ $builder->add('exchange_rate', BigDecimalMoneyType::class, [
'required' => false, 'required' => false,
'label' => 'currency.edit.exchange_rate', 'label' => 'currency.edit.exchange_rate',
'currency' => $this->default_currency, 'currency' => $this->default_currency,

View file

@ -213,11 +213,11 @@ class PricedetailHelper
$val_base = $value; $val_base = $value;
if (null !== $originCurrency) { if (null !== $originCurrency) {
//Without an exchange rate we can not calculate the exchange rate //Without an exchange rate we can not calculate the exchange rate
if (0.0 === (float) $originCurrency->getExchangeRate()) { if ($originCurrency->getExchangeRate() === null || $originCurrency->getExchangeRate()->isZero()) {
return null; return null;
} }
$val_base = bcmul($value, $originCurrency->getExchangeRate(), Pricedetail::PRICE_PRECISION); $val_base = bcmul($value, (string) $originCurrency->getExchangeRate(), Pricedetail::PRICE_PRECISION);
} }
//Convert value in base currency to target currency //Convert value in base currency to target currency
@ -228,7 +228,7 @@ class PricedetailHelper
return null; return null;
} }
$val_target = bcmul($val_base, $targetCurrency->getInverseExchangeRate(), Pricedetail::PRICE_PRECISION); $val_target = bcmul($val_base, (string) $targetCurrency->getInverseExchangeRate(), Pricedetail::PRICE_PRECISION);
} }
return $val_target; return $val_target;

View file

@ -0,0 +1,59 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* 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/>.
*/
namespace App\Validator\Constraints\BigDecimal;
use Brick\Math\BigDecimal;
use Symfony\Component\Validator\Constraints\AbstractComparisonValidator;
use Symfony\Component\Validator\Constraints\GreaterThan;
/**
* Validates values are greater than the previous (>).
*
* @author Daniel Holmes <daniel@danielholmes.org>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class BigDecimalGreaterThanValidator extends AbstractComparisonValidator
{
/**
* {@inheritdoc}
*/
protected function compareValues($value1, $value2)
{
if ($value1 instanceof BigDecimal) {
$value1 = (string) $value1;
}
if ($value2 instanceof BigDecimal) {
$value2 = (string) $value2;
}
return null === $value2 || $value1 > $value2;
}
/**
* {@inheritdoc}
*/
protected function getErrorCode()
{
return GreaterThan::TOO_LOW_ERROR;
}
}

View file

@ -0,0 +1,49 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* 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/>.
*/
namespace App\Validator\Constraints\BigDecimal;
use Symfony\Component\Validator\Constraints\GreaterThan;
use Symfony\Component\Validator\Constraints\GreaterThanValidator;
use Symfony\Component\Validator\Constraints\NumberConstraintTrait;
/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*
* @author Jan Schädlich <jan.schaedlich@sensiolabs.de>
*/
class BigDecimalPositive extends GreaterThan
{
use NumberConstraintTrait;
public $message = 'This value should be positive.';
public function __construct($options = null)
{
parent::__construct($this->configureNumberConstraintOptions($options));
}
public function validatedBy(): string
{
return BigDecimalGreaterThanValidator::class;
}
}

View file

@ -20,7 +20,7 @@
{{ form_row(form.exchange_rate) }} {{ form_row(form.exchange_rate) }}
{% if entity.inverseExchangeRate %} {% if entity.inverseExchangeRate %}
<span class="form-text text-muted offset-3 col-9"> <span class="form-text text-muted offset-3 col-9">
{{ '1'|format_currency(default_currency) }} = {{ entity.inverseExchangeRate | format_currency(entity.isoCode, {fraction_digit: 5}) }} {{ '1'|format_currency(default_currency) }} = {{ entity.inverseExchangeRate.tofloat | format_currency(entity.isoCode, {fraction_digit: 5}) }}
</span> </span>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -43,6 +43,7 @@ declare(strict_types=1);
namespace App\Tests\Entity\PriceSystem; namespace App\Tests\Entity\PriceSystem;
use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Currency;
use Brick\Math\BigDecimal;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class CurrencyTest extends TestCase class CurrencyTest extends TestCase
@ -54,10 +55,10 @@ class CurrencyTest extends TestCase
//By default the inverse exchange rate is not set: //By default the inverse exchange rate is not set:
$this->assertNull($currency->getInverseExchangeRate()); $this->assertNull($currency->getInverseExchangeRate());
$currency->setExchangeRate('0'); $currency->setExchangeRate(BigDecimal::zero());
$this->assertNull($currency->getInverseExchangeRate()); $this->assertNull($currency->getInverseExchangeRate());
$currency->setExchangeRate('1.45643'); $currency->setExchangeRate(BigDecimal::of('1.45643'));
$this->assertSame('0.68661', $currency->getInverseExchangeRate()); $this->assertSame(BigDecimal::of('0.68661'), $currency->getInverseExchangeRate());
} }
} }