Respect the currency of the prices when calculating average part price.

This commit is contained in:
Jan Böhmer 2019-09-01 18:52:22 +02:00
parent a479dc81c4
commit af3dfafe22
5 changed files with 148 additions and 101 deletions

View file

@ -537,68 +537,6 @@ class Part extends AttachmentContainingDBElement
return $this->devices; return $this->devices;
} }
/**
* Get all prices of this part.
*
* This method simply gets the prices of the orderdetails and prepare them.\n
* In the returned array/string there is a price for every supplier.
* @param int $quantity this is the quantity to choose the correct priceinformation
* @param int|null $multiplier * This is the multiplier which will be applied to every single price
* * If you pass NULL, the number from $quantity will be used
* @param bool $hide_obsolete If true, prices from obsolete orderdetails will NOT be returned
*
* @return float[] all prices as an array of floats (if "$delimeter == NULL" & "$float_array == true")
*
* @throws \Exception if there was an error
*/
public function getPrices(int $quantity = 1, $multiplier = null, bool $hide_obsolete = false) : array
{
$prices = array();
foreach ($this->getOrderdetails($hide_obsolete) as $details) {
$prices[] = $details->getPrice($quantity, $multiplier);
}
return $prices;
}
/**
* Get the average price of all orderdetails.
*
* With the $multiplier you're able to multiply the price before it will be returned.
* This is useful if you want to have the price as a string with currency, but multiplied with a factor.
*
* @param int $quantity this is the quantity to choose the correct priceinformations
* @param int|null $multiplier * This is the multiplier which will be applied to every single price
* * If you pass NULL, the number from $quantity will be used
*
* @return float price (if "$as_money_string == false")
*
* @throws \Exception if there was an error
*/
public function getAveragePrice(int $quantity = 1, $multiplier = null) : ?float
{
$prices = $this->getPrices($quantity, $multiplier, true);
//Findout out
$average_price = null;
$count = 0;
foreach ($this->getOrderdetails() as $orderdetail) {
$price = $orderdetail->getPrice(1, null);
if (null !== $price) {
$average_price += $price;
++$count;
}
}
if ($count > 0) {
$average_price /= $count;
}
return $average_price;
}
/** /**
* Checks if this part is marked, for that it needs further review. * Checks if this part is marked, for that it needs further review.
* @return bool * @return bool

View file

@ -217,8 +217,6 @@ class Orderdetail extends DBElement
* *
* @return Pricedetail[]|Collection all pricedetails as a one-dimensional array of Pricedetails objects, * @return Pricedetail[]|Collection all pricedetails as a one-dimensional array of Pricedetails objects,
* sorted by minimum discount quantity * sorted by minimum discount quantity
*
* @throws Exception if there was an error
*/ */
public function getPricedetails(): Collection public function getPricedetails(): Collection
{ {
@ -249,50 +247,30 @@ class Orderdetail extends DBElement
} }
/** /**
* Get the price for a specific quantity. * Get the pricedetail for a specific quantity.
* @param float $quantity this is the quantity to choose the correct pricedetails * @param float $quantity this is the quantity to choose the correct pricedetails
* @param string|float|int $multiplier * This is the multiplier which will be applied to every single price
* * If you pass NULL, the number from $quantity will be used
* *
* @return string|null: the price as a bcmath string. Null if there are no orderdetails for the given quantity * @return Pricedetail|null: the price as a bcmath string. Null if there are no orderdetails for the given quantity
*
* @throws Exception if there are no pricedetails for the choosed quantity
* (for example, there are only one pricedetails with the minimum discount quantity '10',
* but the choosed quantity is '5' --> the price for 5 parts is not defined!)
* @throws Exception if there was an error
*/ */
public function getPrice(float $quantity = 1, $multiplier = 1) : ?string public function getPrice(float $quantity = 1) : ?Pricedetail
{ {
if (($quantity === 0) && ($multiplier === null)) { if ($quantity <= 0) {
return "0.0"; return null;
} }
$all_pricedetails = $this->getPricedetails(); $all_pricedetails = $this->getPricedetails();
if (count($all_pricedetails) == 0) { $correct_pricedetail = null;
return null; foreach ($all_pricedetails as $pricedetail) {
}
$correct_pricedetails = null;
foreach ($all_pricedetails as $pricedetails) {
// choose the correct pricedetails for the choosed quantity ($quantity) // choose the correct pricedetails for the choosed quantity ($quantity)
if ($quantity < $pricedetails->getMinDiscountQuantity()) { if ($quantity < $pricedetail->getMinDiscountQuantity()) {
break; break;
} }
$correct_pricedetails = $pricedetails; $correct_pricedetail = $pricedetail;
} }
if ($correct_pricedetails === null) { return $correct_pricedetail;
return null;
}
if ($multiplier === null) {
$multiplier = $quantity;
}
return $correct_pricedetails->getPricePerUnit($multiplier);
} }
/******************************************************************************** /********************************************************************************

View file

@ -181,7 +181,7 @@ class Pricedetail extends DBElement
/** /**
* Get the price for a single unit in the currency associated with this price detail. * Get the price for a single unit in the currency associated with this price detail.
* *
* @param float $multiplier The returned price (float or string) will be multiplied * @param float|string $multiplier The returned price (float or string) will be multiplied
* with this multiplier. * with this multiplier.
* *
* You will get the price for $multiplier parts. If you want the price which is stored * You will get the price for $multiplier parts. If you want the price which is stored
@ -190,7 +190,7 @@ class Pricedetail extends DBElement
* @return string the price as a bcmath string * @return string the price as a bcmath string
*/ */
public function getPricePerUnit($multiplier = 1) : string public function getPricePerUnit($multiplier = 1.0) : string
{ {
$multiplier = (string) $multiplier; $multiplier = (string) $multiplier;
$tmp = bcmul($this->price, $multiplier, static::PRICE_PRECISION); $tmp = bcmul($this->price, $multiplier, static::PRICE_PRECISION);

View file

@ -31,11 +31,10 @@
namespace App\Services; namespace App\Services;
use App\Entity\Parts\Part;
use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Pricedetail; use App\Entity\PriceInformations\Pricedetail;
use Locale; use Locale;
use Money\Number;
use Symfony\Component\Intl\Currencies;
class PricedetailHelper class PricedetailHelper
{ {
@ -48,6 +47,119 @@ class PricedetailHelper
$this->locale = Locale::getDefault(); $this->locale = Locale::getDefault();
} }
/**
* Determines the highest amount, for which you get additional discount.
* This function determines the highest min_discount_quantity for the given part.
* @param Part $part
* @return float|null
*/
public function getMaxDiscountAmount(Part $part) : ?float
{
$orderdetails = $part->getOrderdetails(true);
$max = 0;
foreach ($orderdetails as $orderdetail) {
$pricedetails = $orderdetail->getPricedetails();
//The orderdetail must have pricedetails, otherwise this will not work!
if (empty($pricedetails)) {
continue;
}
/* Pricedetails in orderdetails are ordered by min discount quantity,
so our first object is our min order amount for the current orderdetail */
$max_amount = $pricedetails->last()->getMinDiscountQuantity();
if ($max_amount > $max) {
$max = $max_amount;
}
}
if ($max > 0) {
return $max;
}
return null;
}
/**
* Determines the minimum amount of the part that can be ordered
* @param Part $part The part for which the minimum order amount should be determined.
* @return float
*/
public function getMinOrderAmount(Part $part) : ?float
{
$orderdetails = $part->getOrderdetails(true);
$min = INF;
foreach ($orderdetails as $orderdetail) {
$pricedetails = $orderdetail->getPricedetails();
//The orderdetail must have pricedetails, otherwise this will not work!
if (count($pricedetails) === 0) {
continue;
}
/* Pricedetails in orderdetails are ordered by min discount quantity,
so our first object is our min order amount for the current orderdetail */
$min_amount = $pricedetails[0]->getMinDiscountQuantity();
if ($min_amount < $min) {
$min = $min_amount;
}
}
if ($min < INF) {
return $min;
}
return null;
}
/**
* Calculates the average price of a part, when ordering the amount $amount.
* @param Part $part The part for which the average price should be calculated.
* @param float $amount The order amount for which the average price should be calculated.
* 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
* price for the given
*/
public function calculateAvgPrice(Part $part, ?float $amount = null, ?Currency $currency = null) : ?string
{
if ($amount === null) {
$amount = $this->getMinOrderAmount($part);
}
if ($amount === null) {
return null;
}
$orderdetails = $part->getOrderdetails(true);
$avg = "0";
$count = 0;
//Find the price for the amount, for the given
foreach ($orderdetails as $orderdetail) {
$pricedetail = $orderdetail->getPrice($amount);
//When we dont have informations about this amount, ignore it
if ($pricedetail === null) {
continue;
}
$avg = bcadd($avg, $this->convertMoneyToCurrency($pricedetail->getPricePerUnit(), $pricedetail->getCurrency(), $currency), Pricedetail::PRICE_PRECISION);
$count++;
}
if ($count === 0) {
return null;
}
return bcdiv($avg, (string) $count, Pricedetail::PRICE_PRECISION);
}
/** /**
* Converts the given value in origin currency to the choosen target currency * Converts the given value in origin currency to the choosen target currency
* @param $value float|string The value that should be converted * @param $value float|string The value that should be converted
@ -60,6 +172,11 @@ class PricedetailHelper
*/ */
public function convertMoneyToCurrency($value, ?Currency $originCurrency = null, ?Currency $targetCurrency = null) : ?string public function convertMoneyToCurrency($value, ?Currency $originCurrency = null, ?Currency $targetCurrency = null) : ?string
{ {
//Skip conversion, if both currencies are same
if ($originCurrency === $targetCurrency) {
return $value;
}
$value = (string) $value; $value = (string) $value;
//Convert value to base currency //Convert value to base currency

View file

@ -34,9 +34,23 @@
<i class="fas fa-microchip fa-fw" ></i> <i class="fas fa-microchip fa-fw" ></i>
<span class="text-muted">{{ part.footprint.fullPath ?? "-"}}</span> <span class="text-muted">{{ part.footprint.fullPath ?? "-"}}</span>
</h6> </h6>
<h6 title="{% trans %}part.avg_price.label{% endtrans %}"> <h6>
<i class="fas fa-money-bill-alt fa-fw"></i> <i class="fas fa-money-bill-alt fa-fw"></i>
<span class="text-muted">{% if part.averagePrice is not null %}{{ part.averagePrice | moneyFormat }}{% else %}-{% endif %}</span> <span class="text-muted">
{% set min_order_amount = pricedetail_helper.minOrderAmount(part) %}
{% set max_order_amount = pricedetail_helper.maxDiscountAmount(part) %}
{% set max_order_price = pricedetail_helper.calculateAvgPrice(part, max_order_amount) %}
{% if max_order_price is not null %}
<span title="{% trans %}part.avg_price.label{% endtrans %} {{ max_order_amount | amountFormat(part.partUnit) }}">{{ max_order_price | moneyFormat }}</span>
{% if min_order_amount < max_order_amount %}
<span> - </span>
<span title="{% trans %}part.avg_price.label{% endtrans %} {{ min_order_amount | amountFormat(part.partUnit) }}">{{pricedetail_helper.calculateAvgPrice(part, min_order_amount) | moneyFormat }}</span>
{% endif %}
{% endif %}
</span>
</h6> </h6>
{# {#
{% if part.comment != "" %} {% if part.comment != "" %}