mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-23 10:18:56 +02:00
Respect the currency of the prices when calculating average part price.
This commit is contained in:
parent
a479dc81c4
commit
af3dfafe22
5 changed files with 148 additions and 101 deletions
|
@ -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
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/********************************************************************************
|
/********************************************************************************
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 != "" %}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue