Skip to content

Add Price and Extended price to Project Bom#1221

Open
whinis wants to merge 2 commits intoPart-DB:masterfrom
whinis:add-bom-price
Open

Add Price and Extended price to Project Bom#1221
whinis wants to merge 2 commits intoPart-DB:masterfrom
whinis:add-bom-price

Conversation

@whinis
Copy link

@whinis whinis commented Feb 2, 2026

This seems to be one of the most requested changes and I also needed this particular feature.
requested in #603 and others in discussion. So this allows one to see the price of each item in the bom if it exists in any way. It currently selects just the first supplier and the first (likely most expensive) unit price for the part.

@whinis whinis force-pushed the add-bom-price branch 4 times, most recently from 166bcb5 to 35262a2 Compare February 4, 2026 04:08
@jbtronics
Copy link
Member

Thanks,

The PricedetailHelper has an calculateAvg() price to calculate an average price across all suppliers, it also handles currency conversions correctly.
Also you should use the correct Intl Number Formatter to get the correctly formatted price for users locale. The currency symbol should be included, to mark which currency this is.

Unfortunatley the whole price infrastructure in Part-DB is quite limited and ideally would need some heavy rewrites, with some more powerful functions to make things like this easier...

@mkne mkne mentioned this pull request Feb 19, 2026
@MayNiklas
Copy link
Contributor

The PricedetailHelper has an calculateAvg() price to calculate an average price across all suppliers, it also handles currency conversions correctly.

Just a random thought:
When calculating the price of a project, I would prefer the price being worse compared to it being too good.
This way, we don't run into the risk of thinking something is cheaper than it actually is.

The complicated thing about pricing: it won't reflect the actual price we bought parts for & sometimes our stock even might contain the same part bought for different prices.

I would love the price estimation to reflect what I'm able to buy the parts now for:
Instead of using the average price, which might result in a price being too good, I would prefer to use the prices of the amounts we actually require to build a fixed amount of units.

Long story short:
I don't know how others think about it, but I would prefer prices to be higher compared tp them being too low.
I would even be fine with PartDB just using the more expensive prices for lower amounts of parts, instead of using the price average.

I know prices in PartDB are complicated!
Of any specific help is required, I would love to help!

@whinis
Copy link
Author

whinis commented Mar 13, 2026

The PricedetailHelper has an calculateAvg() price to calculate an average price across all suppliers, it also handles currency conversions correctly.

Just a random thought: When calculating the price of a project, I would prefer the price being worse compared to it being too good. This way, we don't run into the risk of thinking something is cheaper than it actually is.

The complicated thing about pricing: it won't reflect the actual price we bought parts for & sometimes our stock even might contain the same part bought for different prices.

I would love the price estimation to reflect what I'm able to buy the parts now for: Instead of using the average price, which might result in a price being too good, I would prefer to use the prices of the amounts we actually require to build a fixed amount of units.

Long story short: I don't know how others think about it, but I would prefer prices to be higher compared tp them being too low. I would even be fine with PartDB just using the more expensive prices for lower amounts of parts, instead of using the price average.

I would love the price estimation to reflect what I'm able to buy the parts now for

Thats what the PR essentially does or theoretically does. I could force it but it currently gets the first price which atleast in my setup tends to be qty 1 price.

@MayNiklas
Copy link
Contributor

@whinis I implemented the changes @jbtronics asked for.
Should I share them with you so you can commit them, adding me as a co-author?

@MayNiklas
Copy link
Contributor

My previous comment can be ignored - I realised how average price works! :)

@MayNiklas
Copy link
Contributor

@whinis If you want to, you can commit my patch integrating the requested changes.
Save the following diff as a file commit.diff.
Then use git am --3way commit.diff.
git am is used to mailbox patches :)

The changes greatly simplify the price calculation!
I have to agree with @jbtronics - this is much nicer!

From 44a9aa16b312f64b83777efd8c369a8ce3805310 Mon Sep 17 00:00:00 2001
From: MayNiklas <info@niklas-steffen.de>
Date: Fri, 13 Mar 2026 21:15:25 +0100
Subject: [PATCH] project prices: use calculateAvg


diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php
index 3acef03f..ec1f5ff4 100644
--- a/src/DataTables/ProjectBomEntriesDataTable.php
+++ b/src/DataTables/ProjectBomEntriesDataTable.php
@@ -39,12 +39,14 @@ use Omines\DataTablesBundle\Column\TextColumn;
 use Omines\DataTablesBundle\DataTable;
 use Omines\DataTablesBundle\DataTableTypeInterface;
 use Symfony\Contracts\Translation\TranslatorInterface;
+use App\Services\Parts\PricedetailHelper;
 use Brick\Math\BigDecimal;
 
 class ProjectBomEntriesDataTable implements DataTableTypeInterface
 {
     public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper,
-        protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter)
+        protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter,
+        protected PricedetailHelper $pricedetailHelper)
     {
     }
 
@@ -183,62 +185,18 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
             ->add('price', TextColumn::class, [
                 'label' => 'project.bom.price',
                 'render' => function ($value, ProjectBOMEntry $context) {
-                    // Let's attempt to get the part, if we don't we will just assume zero
-                    $part = $context->getPart();
-                    $price = BigDecimal::zero();
-                    $pricedetails = null;
-                    $order = null;
-                    // Check if we get a part and get the first order if so
-                    // if not see if there is a non-part price set
-                    if($part) {
-                        $order = $context->getPart()->getOrderdetails()->first();
-                    } else if($context->getPrice() !== null) {
-                        $price = $context->getPrice();
-                    }
-
-                    // check if there is an order, if so get the first pricedetail of the order
-                    if($order!==null && $order !== false) {
-                        $pricedetails = $order->getPricedetails()->first();
-                    }
-
-                    // check if there is a pricedetail, if so get the first price
-                    if($pricedetails !== null && $pricedetails !== false) {
-                        $price = $pricedetails->getPrice();
-                    }
+                    $price = $this->getBomEntryUnitPrice($context);
 
-                    // return the price
-                    return  htmlspecialchars(number_format($price->toFloat(),2));
+                    return htmlspecialchars(number_format($price->toFloat(), 2));
                 },
                 'visible' => false,
             ])
             ->add('ext_price', TextColumn::class, [
                 'label' => 'project.bom.ext_price',
                 'render' => function ($value, ProjectBOMEntry $context) {
-                    // Let's attempt to get the part, if we don't we will just assume zero
-                    $part = $context->getPart();
-                    $price = BigDecimal::zero();
-                    $pricedetails = null;
-                    $order = null;
-                    // Check if we get a part and get the first order if so
-                    // if not see if there is a non-part price set
-                    if($part) {
-                        $order = $context->getPart()->getOrderdetails()->first();
-                    } else if($context->getPrice() !== null) {
-                        $price = $context->getPrice();
-                    }
-
-                    // check if there is an order, if so get the first pricedetail of the order
-                    if($order!==null && $order !== false) {
-                        $pricedetails = $order->getPricedetails()->first();
-                    }
+                    $price = $this->getBomEntryUnitPrice($context);
 
-                    // check if there is a pricedetail, if so get the first price
-                    if($pricedetails !== null && $pricedetails !== false) {
-                        $price = $pricedetails->getPrice();
-                    }
-
-                    // return the price
-                    return  htmlspecialchars(number_format($price->toFloat() * $context->getQuantity(),2));
+                    return htmlspecialchars(number_format($price->toFloat() * $context->getQuantity(), 2));
                 },
             ])
 
@@ -268,6 +226,16 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
         ]);
     }
 
+    private function getBomEntryUnitPrice(ProjectBOMEntry $entry): BigDecimal
+    {
+        if ($entry->getPart() instanceof Part) {
+            return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $entry->getQuantity()) ?? BigDecimal::zero();
+        }
+
+        return $entry->getPrice() ?? BigDecimal::zero();
+    }
+
+
     private function getQuery(QueryBuilder $builder, array $options): void
     {
         $builder->select('bom_entry')

@whinis
Copy link
Author

whinis commented Mar 13, 2026

@MayNiklas Alright I incorporated that. It does look nicer in this case.

@MayNiklas
Copy link
Contributor

@jbtronics this PR should be ready for a re-review :)

@MayNiklas
Copy link
Contributor

@jbtronics I just noticed, that pricedetailHelper->calculateAvgPrice does not work as expected for smaller quantities, when quantity < minimum order amount.
In those cases, the function returns NULL, leading to a price of 0,00 being used.
Would it be possible to fix that behaviour? Or does anything speak against it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants