diff --git a/src/Entity/PriceInformations/Pricedetail.php b/src/Entity/PriceInformations/Pricedetail.php index 436a14ea..983361ed 100644 --- a/src/Entity/PriceInformations/Pricedetail.php +++ b/src/Entity/PriceInformations/Pricedetail.php @@ -53,7 +53,10 @@ namespace App\Entity\PriceInformations; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\TimestampTrait; use App\Entity\Contracts\TimeStampableInterface; +use App\Validator\Constraints\BigDecimal\BigDecimalPositive; use App\Validator\Constraints\Selectable; +use Brick\Math\BigDecimal; +use Brick\Math\RoundingMode; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -74,10 +77,10 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface /** * @var string The price related to the detail. (Given in the selected currency) - * @ORM\Column(type="decimal", precision=11, scale=5) - * @Assert\Positive() + * @ORM\Column(type="big_decimal", precision=11, scale=5) + * @BigDecimalPositive() */ - protected $price = '0.0'; + protected $price; /** * @var ?Currency The currency used for the current price information. @@ -118,7 +121,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface public function __construct() { - bcscale(static::PRICE_PRECISION); + $this->price = BigDecimal::zero()->toScale(self::PRICE_PRECISION); } public function __clone() @@ -149,9 +152,9 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface * Returns the price associated with this pricedetail. * It is given in current currency and for the price related quantity. * - * @return string the price as string, like returned raw from DB + * @return BigDecimal the price as BigDecimal object, like returned raw from DB */ - public function getPrice(): string + public function getPrice(): BigDecimal { return $this->price; } @@ -159,23 +162,20 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface /** * Get the price for a single unit in the currency associated with this price detail. * - * @param float|string $multiplier The returned price (float or string) will be multiplied + * @param float|string|BigDecimal $multiplier The returned price (float or string) will be multiplied * with this multiplier. * * You will get the price for $multiplier parts. If you want the price which is stored * in the database, you have to pass the "price_related_quantity" count as $multiplier. - * @param float|string $multiplier The returned price (float or string) will be multiplied - * with this multiplier. * - * @return string|null the price as a bcmath string + * @return BigDecimal the price as a bcmath string */ - public function getPricePerUnit($multiplier = 1.0): ?string + public function getPricePerUnit($multiplier = 1.0): BigDecimal { - $multiplier = (string) $multiplier; - $tmp = bcmul($this->price, $multiplier, static::PRICE_PRECISION); + $tmp = BigDecimal::of($multiplier); + $tmp = $tmp->multipliedBy($this->price); - return bcdiv($tmp, (string) $this->price_related_quantity, static::PRICE_PRECISION); - //return ($this->price * $multiplier) / $this->price_related_quantity; + return $tmp->dividedBy($this->price_related_quantity, static::PRICE_PRECISION, RoundingMode::HALF_UP); } /** @@ -265,7 +265,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface /** * Set the price. * - * @param string $new_price the new price as a float number + * @param BigDecimal $new_price the new price as a float number * * * This is the price for "price_related_quantity" parts!! * * Example: if "price_related_quantity" is '10', @@ -273,15 +273,9 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface * * @return $this */ - public function setPrice(string $new_price): self + public function setPrice(BigDecimal $new_price): self { - //Assert::natural($new_price, 'The new price must be positive! Got %s!'); - - /* Just a little hack to ensure that price has 5 digits after decimal point, - so that DB does not detect changes, when something like 0.4 is passed - Third parameter must have the scale value of decimal column. */ - $this->price = bcmul($new_price, '1.0', static::PRICE_PRECISION); - + $this->price = $new_price->toScale(self::PRICE_PRECISION, RoundingMode::HALF_UP); return $this; } diff --git a/src/Form/Part/PricedetailType.php b/src/Form/Part/PricedetailType.php index a6d9a371..440660e3 100644 --- a/src/Form/Part/PricedetailType.php +++ b/src/Form/Part/PricedetailType.php @@ -44,10 +44,11 @@ namespace App\Form\Part; use App\Entity\Parts\MeasurementUnit; use App\Entity\PriceInformations\Pricedetail; +use App\Form\Type\BigDecimalMoneyType; +use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\SIUnitType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -70,7 +71,7 @@ class PricedetailType extends AbstractType 'class' => 'form-control-sm', ], ]); - $builder->add('price', NumberType::class, [ + $builder->add('price', BigDecimalNumberType::class, [ 'label' => false, 'scale' => 5, 'html5' => true, diff --git a/src/Form/Type/BigDecimalNumberType.php b/src/Form/Type/BigDecimalNumberType.php new file mode 100644 index 00000000..7759e121 --- /dev/null +++ b/src/Form/Type/BigDecimalNumberType.php @@ -0,0 +1,63 @@ +. + */ + +namespace App\Form\Type; + + +use Brick\Math\BigDecimal; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\FormBuilderInterface; + +class BigDecimalNumberType extends AbstractType implements DataTransformerInterface +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addModelTransformer($this); + } + + public function getParent() + { + return NumberType::class; + } + + public function transform($value) + { + if ($value === null) { + return null; + } + + if ($value instanceof BigDecimal) { + return (string) $value; + } + + return $value; + } + + public function reverseTransform($value) + { + if ($value === null) { + return null; + } + + return BigDecimal::of($value); + } +} \ No newline at end of file diff --git a/src/Services/PricedetailHelper.php b/src/Services/PricedetailHelper.php index 0da8eb31..5de1ba0f 100644 --- a/src/Services/PricedetailHelper.php +++ b/src/Services/PricedetailHelper.php @@ -45,6 +45,8 @@ namespace App\Services; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Pricedetail; +use Brick\Math\BigDecimal; +use Brick\Math\RoundingMode; use Doctrine\ORM\PersistentCollection; use Locale; @@ -147,10 +149,10 @@ class PricedetailHelper * If set to null, the mininmum order amount for the part is used. * @param Currency|null $currency The currency in which the average price should be calculated * - * @return string|null The Average price as bcmath string. Returns null, if it was not possible to calculate the + * @return BigDecimal|null The Average price as bcmath string. Returns null, if it was not possible to calculate the * price for the given */ - public function calculateAvgPrice(Part $part, ?float $amount = null, ?Currency $currency = null): ?string + public function calculateAvgPrice(Part $part, ?float $amount = null, ?Currency $currency = null): ?BigDecimal { if (null === $amount) { $amount = $this->getMinOrderAmount($part); @@ -162,7 +164,7 @@ class PricedetailHelper $orderdetails = $part->getOrderdetails(true); - $avg = '0'; + $avg = BigDecimal::zero(); $count = 0; //Find the price for the amount, for the given @@ -177,7 +179,7 @@ class PricedetailHelper $converted = $this->convertMoneyToCurrency($pricedetail->getPricePerUnit(), $pricedetail->getCurrency(), $currency); //Ignore price informations that can not be converted to base currency. if (null !== $converted) { - $avg = bcadd($avg, $converted, Pricedetail::PRICE_PRECISION); + $avg = $avg->plus($converted); ++$count; } } @@ -186,7 +188,7 @@ class PricedetailHelper return null; } - return bcdiv($avg, (string) $count, Pricedetail::PRICE_PRECISION); + return $avg->dividedBy($count)->toScale(Pricedetail::PRICE_PRECISION); } /** @@ -197,40 +199,38 @@ class PricedetailHelper * @param Currency|null $targetCurrency The target currency, to which $value should be converted. * Set to null, to use global base currency. * - * @return string|null The value in $targetCurrency given as bcmath string. + * @return BigDecimal|null The value in $targetCurrency given as bcmath string. * Returns null, if it was not possible to convert between both values (e.g. when the exchange rates are missing) */ - public function convertMoneyToCurrency($value, ?Currency $originCurrency = null, ?Currency $targetCurrency = null): ?string + public function convertMoneyToCurrency(BigDecimal $value, ?Currency $originCurrency = null, ?Currency $targetCurrency = null): ?BigDecimal { //Skip conversion, if both currencies are same if ($originCurrency === $targetCurrency) { return $value; } - $value = (string) $value; - - //Convert value to base currency $val_base = $value; + //Convert value to base currency if (null !== $originCurrency) { //Without an exchange rate we can not calculate the exchange rate if ($originCurrency->getExchangeRate() === null || $originCurrency->getExchangeRate()->isZero()) { return null; } - $val_base = bcmul($value, (string) $originCurrency->getExchangeRate(), Pricedetail::PRICE_PRECISION); + $val_base = $value->multipliedBy($originCurrency->getExchangeRate()); } - //Convert value in base currency to target currency $val_target = $val_base; + //Convert value in base currency to target currency if (null !== $targetCurrency) { //Without an exchange rate we can not calculate the exchange rate if (null === $targetCurrency->getExchangeRate()) { return null; } - $val_target = bcmul($val_base, (string) $targetCurrency->getInverseExchangeRate(), Pricedetail::PRICE_PRECISION); + $val_target = $val_base->multipliedBy($targetCurrency->getInverseExchangeRate()); } - return $val_target; + return $val_target->toScale(Pricedetail::PRICE_PRECISION, RoundingMode::HALF_UP); } } diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index 966b7a99..8ed0c883 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -53,6 +53,7 @@ use App\Services\MarkdownParser; use App\Services\MoneyFormatter; use App\Services\SIFormatter; use App\Services\Trees\TreeViewGenerator; +use Brick\Math\BigDecimal; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\AbstractExtension; @@ -153,6 +154,10 @@ class AppExtension extends AbstractExtension public function formatCurrency($amount, ?Currency $currency = null, int $decimals = 5) { + if ($amount instanceof BigDecimal) { + $amount = (string) $amount; + } + return $this->moneyFormatter->format($amount, $currency, $decimals); } diff --git a/templates/Parts/info/_order_infos.html.twig b/templates/Parts/info/_order_infos.html.twig index 2a26689c..e050120a 100644 --- a/templates/Parts/info/_order_infos.html.twig +++ b/templates/Parts/info/_order_infos.html.twig @@ -38,18 +38,21 @@
{% for detail in order.pricedetails %}