Repository: inventree/InvenTree

Issue:
Use purchase_price for BOM cost calculation
Current BOM pricing approach implies that all BOM items should be supplier parts with pre-filled price breaks. And it gives you the cost of item if you buy all parts from corresponding suppliers.
While in my case, I assemble my projects from parts I already have. So I would like to know how much it will cost to assemble this item based on parts I have in stock (purchase price).

Please consider replacing supplier price with purchase price for the BOM pricing view. This could be the global/user settings option.
@matmair what do you think?

Use purchase_price for BOM cost calculation
Current BOM pricing approach implies that all BOM items should be supplier parts with pre-filled price breaks. And it gives you the cost of item if you buy all parts from corresponding suppliers.
While in my case, I assemble my projects from parts I already have. So I would like to know how much it will cost to assemble this item based on parts I have in stock (purchase price).

Please consider replacing supplier price with purchase price for the BOM pricing view. This could be the global/user settings option.
@matmair what do you think?



Discussion:
@sintech that is not quite as simple as it sounds. A lot of EE people collect parts from different vendors at different price points. So if you have a part of which you bought 5 at 4ct, 10 at 6ct and 40 at 5ct - how much should InvenTree charge for 3 pieces? Just 4 because it is the middle? A weighted middle? Should that change every time you make a PO? What if you use parts like this in assemblies and you have a complex part with 100s of subassemblies? Should we calculated the price of each part bottom up? Would anything ever have a fixed price?

Our solution to that currently is the internal price: If activated you can set a price that should be used for BOM calculations. The pricing section of a part provides you with a graph of the cost at which you bought inventory/stock so you can choose a good middle-ground you should charge.
@sintech if you have a idea how the problems stated above can be handled I am happy to review a PR.
@matmair, I got you point and it is fair one.
But can we just make min and max price as it is currently done in code. As I can see, `get_supplier_price_range` loops over all suppliers, gets prices based on quantity and finally returns one min and max price.

Internal price is a good tool for something like workforce or price override. I don't want to abuse it by copying data I already have in DB. 
@matmair I have to admit that the current pricing data present in the current version of InvenTree is quite complex and is not documented... I personally just discovered that the internal pricing option was disabled on our instance and found it in the setting. But still unsure what it does!

If I look at a BOM, I see we have 3 columns for pricing: purchase price average, purchase price range and buy price (stock price?).
And no idea what the "dollar sign" button calculation form uses for part pricing...

The pricing graph is still unclear too:

![image](https://user-images.githubusercontent.com/4020546/127882617-241a39a0-363f-4587-bb61-24e2ae306f1c.png)

"Single Price" vs "Part Single Price?
Information icon next to "Stock Pricing" does not overlay any information.

"Part Pricing" form is also not telling where does the price comes from, even though I _know_ it is supplier price break.
> I have to admit that the current pricing data present in the current version of InvenTree is quite complex and is not documented...


@eeintech I tired to fix the situation with screenshots before tackling the docs but did not get anywhere so here are some docs for pricing https://github.com/inventree/inventree-docs/pull/146

> I personally just discovered that the internal pricing option was disabled on our instance and found it in the setting. But still unsure what it does!

Internal prices where disabled by default because it adds complexity hobbyists don’t need in general.


> The pricing graph is still unclear too:
> 
> ![image](https://user-images.githubusercontent.com/4020546/127882617-241a39a0-363f-4587-bb61-24e2ae306f1c.png)

Difficult to understand with just one data-point 👼. This only gets useful if there are a few data points from previous purchases. What should be explained more in the information bubble? Should it be renamed to 'unit price'?
 
> "Single Price" vs "Part Single Price?
> Information icon next to "Stock Pricing" does not overlay any information.

In the current master? Seems like a bug - there should be a info text appearing.

> "Part Pricing" form is also not telling where does the price comes from, even though I _know_ it is supplier price break.

I do not understand what you mean - the selling price is used as that is the price that a part is sold for. Should we but a info-marker there?


> @matmair, I got you point and it is fair one.
> But can we just make min and max price as it is currently done in code. As I can see, `get_supplier_price_range` loops over all suppliers, gets prices based on quantity and finally returns one min and max price.

 @sintech You surely can implement that - I would ask to make that a setting though as using stock prices where many current users will not have set purchase prices for all parts seems dangerous. I personally would never price anything by past prices but by what it costs me to buy new ones - to much change in the IC and sensor markets.

> Internal price is a good tool for something like workforce or price override. I don't want to abuse it by copying data I already have in DB.

I think that depends. I use them for each part as it makes the resulting prices much more inline with what you have to spend to get them. Parts just do not cost only buying price. Purchase price should only be used as a general trend reference in my mind. But that really depends on how you calculate your prices and what your business model is.

> What should be explained more in the information bubble? Should it be renamed to 'unit price'?

"Unit Price" terminology makes more sense yes. 
What about:
- "Part Single Price" --> "Supplier Unit Cost"
- "Single Price" --> "Purchase Unit Price"
- "Single Price Difference" --> "Unit Price-Cost Difference"

> In the current master? Seems like a bug - there should be a info text appearing.

Yes in master, looks like a regression indeed.

> I do not understand what you mean - the selling price is used as that is the price that a part is sold for. Should we but a info-marker there?

In the part "Prices" tab, one can calculate the cost for X unit using the embedded form:

![image](https://user-images.githubusercontent.com/4020546/128032525-208f7c24-bd4e-4d19-bfb1-9a4a9e2415cc.png)

Here the result is clear: this is the theoretical price range to be paid from suppliers to get X quantity of that part.

In the "Part Pricing" form, the result is the same:

![image](https://user-images.githubusercontent.com/4020546/128032991-2ed9de84-dad3-4dea-b497-bd1fb88497a4.png)

So the pricing/cost is quite clear here on for a `component` part.

Now if I go to an `assembly` part to calculate BOM price/cost, this is where the confusion happens.

The "Prices" tab shows:

![image](https://user-images.githubusercontent.com/4020546/128033371-57f0081e-bf10-4f8c-a764-61a52e6f765c.png)

and the "Part Pricing" form is consistent:

![image](https://user-images.githubusercontent.com/4020546/128033440-ec1d3fb9-65c6-4e02-b9c9-b9d7b759e3a3.png)

In the BOM, there are 3 columns related to pricing ("Purchase Price Range", "Purchase Price Average" and "Buy Price"):

![image](https://user-images.githubusercontent.com/4020546/128033781-632fd1fe-bbe8-4a42-a1f4-9126738f30b7.png)

Therefore one can be wondering: is the BOM cost calculated from the supplier cost data or from the purchase price ranges?

By the way, "Buy Price" should actually be named "Supplier Cost" don't you think? This data is coming from supplier cost break, not from the purchase price.

I would be happy to help consolidate the terminologies and explain the source of price/cost data in each form, let me know.
The new pricing documentation will definitely help out a lot, great stuff!
@eeintech thanks for the suggestions, I made a PR with the changes. I would love some help with naming and documenting as I struggle with that a bit. I have a minor in (German) budgeting / accounting, so I sometimes think a bit more complex than needed for InvenTree and do not explain enough.
@matmair Sounds good, I do not have any degree in finance so your experience is definitely welcome!
So @sintech are you intrested in implementing this? If yes please include a setting to enable it
@matmair yes, I will try to implement it by myself, but it may take some time.
It was my original intention to make it optional.
@sintech great! If you have questions do not hesitate, we are here to help!
@sintech that is not quite as simple as it sounds. A lot of EE people collect parts from different vendors at different price points. So if you have a part of which you bought 5 at 4ct, 10 at 6ct and 40 at 5ct - how much should InvenTree charge for 3 pieces? Just 4 because it is the middle? A weighted middle? Should that change every time you make a PO? What if you use parts like this in assemblies and you have a complex part with 100s of subassemblies? Should we calculated the price of each part bottom up? Would anything ever have a fixed price?

Our solution to that currently is the internal price: If activated you can set a price that should be used for BOM calculations. The pricing section of a part provides you with a graph of the cost at which you bought inventory/stock so you can choose a good middle-ground you should charge.
@sintech if you have a idea how the problems stated above can be handled I am happy to review a PR.
@matmair, I got you point and it is fair one.
But can we just make min and max price as it is currently done in code. As I can see, `get_supplier_price_range` loops over all suppliers, gets prices based on quantity and finally returns one min and max price.

Internal price is a good tool for something like workforce or price override. I don't want to abuse it by copying data I already have in DB. 
@matmair I have to admit that the current pricing data present in the current version of InvenTree is quite complex and is not documented... I personally just discovered that the internal pricing option was disabled on our instance and found it in the setting. But still unsure what it does!

If I look at a BOM, I see we have 3 columns for pricing: purchase price average, purchase price range and buy price (stock price?).
And no idea what the "dollar sign" button calculation form uses for part pricing...

The pricing graph is still unclear too:

![image](https://user-images.githubusercontent.com/4020546/127882617-241a39a0-363f-4587-bb61-24e2ae306f1c.png)

"Single Price" vs "Part Single Price?
Information icon next to "Stock Pricing" does not overlay any information.

"Part Pricing" form is also not telling where does the price comes from, even though I _know_ it is supplier price break.
> I have to admit that the current pricing data present in the current version of InvenTree is quite complex and is not documented...


@eeintech I tired to fix the situation with screenshots before tackling the docs but did not get anywhere so here are some docs for pricing https://github.com/inventree/inventree-docs/pull/146

> I personally just discovered that the internal pricing option was disabled on our instance and found it in the setting. But still unsure what it does!

Internal prices where disabled by default because it adds complexity hobbyists don’t need in general.


> The pricing graph is still unclear too:
> 
> ![image](https://user-images.githubusercontent.com/4020546/127882617-241a39a0-363f-4587-bb61-24e2ae306f1c.png)

Difficult to understand with just one data-point 👼. This only gets useful if there are a few data points from previous purchases. What should be explained more in the information bubble? Should it be renamed to 'unit price'?
 
> "Single Price" vs "Part Single Price?
> Information icon next to "Stock Pricing" does not overlay any information.

In the current master? Seems like a bug - there should be a info text appearing.

> "Part Pricing" form is also not telling where does the price comes from, even though I _know_ it is supplier price break.

I do not understand what you mean - the selling price is used as that is the price that a part is sold for. Should we but a info-marker there?


> @matmair, I got you point and it is fair one.
> But can we just make min and max price as it is currently done in code. As I can see, `get_supplier_price_range` loops over all suppliers, gets prices based on quantity and finally returns one min and max price.

 @sintech You surely can implement that - I would ask to make that a setting though as using stock prices where many current users will not have set purchase prices for all parts seems dangerous. I personally would never price anything by past prices but by what it costs me to buy new ones - to much change in the IC and sensor markets.

> Internal price is a good tool for something like workforce or price override. I don't want to abuse it by copying data I already have in DB.

I think that depends. I use them for each part as it makes the resulting prices much more inline with what you have to spend to get them. Parts just do not cost only buying price. Purchase price should only be used as a general trend reference in my mind. But that really depends on how you calculate your prices and what your business model is.

> What should be explained more in the information bubble? Should it be renamed to 'unit price'?

"Unit Price" terminology makes more sense yes. 
What about:
- "Part Single Price" --> "Supplier Unit Cost"
- "Single Price" --> "Purchase Unit Price"
- "Single Price Difference" --> "Unit Price-Cost Difference"

> In the current master? Seems like a bug - there should be a info text appearing.

Yes in master, looks like a regression indeed.

> I do not understand what you mean - the selling price is used as that is the price that a part is sold for. Should we but a info-marker there?

In the part "Prices" tab, one can calculate the cost for X unit using the embedded form:

![image](https://user-images.githubusercontent.com/4020546/128032525-208f7c24-bd4e-4d19-bfb1-9a4a9e2415cc.png)

Here the result is clear: this is the theoretical price range to be paid from suppliers to get X quantity of that part.

In the "Part Pricing" form, the result is the same:

![image](https://user-images.githubusercontent.com/4020546/128032991-2ed9de84-dad3-4dea-b497-bd1fb88497a4.png)

So the pricing/cost is quite clear here on for a `component` part.

Now if I go to an `assembly` part to calculate BOM price/cost, this is where the confusion happens.

The "Prices" tab shows:

![image](https://user-images.githubusercontent.com/4020546/128033371-57f0081e-bf10-4f8c-a764-61a52e6f765c.png)

and the "Part Pricing" form is consistent:

![image](https://user-images.githubusercontent.com/4020546/128033440-ec1d3fb9-65c6-4e02-b9c9-b9d7b759e3a3.png)

In the BOM, there are 3 columns related to pricing ("Purchase Price Range", "Purchase Price Average" and "Buy Price"):

![image](https://user-images.githubusercontent.com/4020546/128033781-632fd1fe-bbe8-4a42-a1f4-9126738f30b7.png)

Therefore one can be wondering: is the BOM cost calculated from the supplier cost data or from the purchase price ranges?

By the way, "Buy Price" should actually be named "Supplier Cost" don't you think? This data is coming from supplier cost break, not from the purchase price.

I would be happy to help consolidate the terminologies and explain the source of price/cost data in each form, let me know.
The new pricing documentation will definitely help out a lot, great stuff!
@eeintech thanks for the suggestions, I made a PR with the changes. I would love some help with naming and documenting as I struggle with that a bit. I have a minor in (German) budgeting / accounting, so I sometimes think a bit more complex than needed for InvenTree and do not explain enough.
@matmair Sounds good, I do not have any degree in finance so your experience is definitely welcome!
So @sintech are you intrested in implementing this? If yes please include a setting to enable it
@matmair yes, I will try to implement it by myself, but it may take some time.
It was my original intention to make it optional.
@sintech great! If you have questions do not hesitate, we are here to help!

Resolving patch:
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -35,6 +35,8 @@
 from decimal import Decimal, InvalidOperation
 from datetime import datetime
 import hashlib
+from djmoney.contrib.exchange.models import convert_money
+from common.settings import currency_code_default
 
 from InvenTree import helpers
 from InvenTree import validators
@@ -1514,7 +1516,7 @@ def get_supplier_price_range(self, quantity=1):
 
         return (min_price, max_price)
 
-    def get_bom_price_range(self, quantity=1, internal=False):
+    def get_bom_price_range(self, quantity=1, internal=False, purchase=False):
         """ Return the price range of the BOM for this part.
         Adds the minimum price for all components in the BOM.
 
@@ -1531,7 +1533,7 @@ def get_bom_price_range(self, quantity=1, internal=False):
                 print("Warning: Item contains itself in BOM")
                 continue
 
-            prices = item.sub_part.get_price_range(quantity * item.quantity, internal=internal)
+            prices = item.sub_part.get_price_range(quantity * item.quantity, internal=internal, purchase=purchase)
 
             if prices is None:
                 continue
@@ -1555,16 +1557,17 @@ def get_bom_price_range(self, quantity=1, internal=False):
 
         return (min_price, max_price)
 
-    def get_price_range(self, quantity=1, buy=True, bom=True, internal=False):
+    def get_price_range(self, quantity=1, buy=True, bom=True, internal=False, purchase=False):
 
         """ Return the price range for this part. This price can be either:
 
         - Supplier price (if purchased from suppliers)
         - BOM price (if built from other parts)
         - Internal price (if set for the part)
+        - Purchase price (if set for the part)
 
         Returns:
-            Minimum of the supplier, BOM or internal price. If no pricing available, returns None
+            Minimum of the supplier, BOM, internal or purchase price. If no pricing available, returns None
         """
 
         # only get internal price if set and should be used
@@ -1572,6 +1575,12 @@ def get_price_range(self, quantity=1, buy=True, bom=True, internal=False):
             internal_price = self.get_internal_price(quantity)
             return internal_price, internal_price
 
+        # only get purchase price if set and should be used
+        if purchase:
+            purchase_price = self.get_purchase_price(quantity)
+            if purchase_price:
+                return purchase_price
+
         buy_price_range = self.get_supplier_price_range(quantity) if buy else None
         bom_price_range = self.get_bom_price_range(quantity, internal=internal) if bom else None
 
@@ -1641,6 +1650,13 @@ def internal_price_breaks(self):
     def internal_unit_pricing(self):
         return self.get_internal_price(1)
 
+    def get_purchase_price(self, quantity):
+        currency = currency_code_default()
+        prices = [convert_money(item.purchase_price, currency).amount for item in self.stock_items.all() if item.purchase_price]
+        if prices:
+            return min(prices) * quantity, max(prices) * quantity
+        return None
+
     @transaction.atomic
     def copy_bom_from(self, other, clear=True, **kwargs):
         """
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -1351,6 +1351,7 @@ def get_pricing(self, quantity=1, currency=None):
 
             use_internal = InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
             bom_price = part.get_bom_price_range(quantity, internal=use_internal)
+            purchase_price = part.get_bom_price_range(quantity, purchase=True)
 
             if bom_price is not None:
                 min_bom_price, max_bom_price = bom_price
@@ -1358,19 +1359,26 @@ def get_pricing(self, quantity=1, currency=None):
                 min_bom_price /= scaler
                 max_bom_price /= scaler
 
-                min_unit_bom_price = round(min_bom_price / quantity, 3)
-                max_unit_bom_price = round(max_bom_price / quantity, 3)
-
-                min_bom_price = round(min_bom_price, 3)
-                max_bom_price = round(max_bom_price, 3)
-
                 if min_bom_price:
-                    ctx['min_total_bom_price'] = min_bom_price
-                    ctx['min_unit_bom_price'] = min_unit_bom_price
+                    ctx['min_total_bom_price'] = round(min_bom_price, 3)
+                    ctx['min_unit_bom_price'] = round(min_bom_price / quantity, 3)
 
                 if max_bom_price:
-                    ctx['max_total_bom_price'] = max_bom_price
-                    ctx['max_unit_bom_price'] = max_unit_bom_price
+                    ctx['max_total_bom_price'] = round(max_bom_price, 3)
+                    ctx['max_unit_bom_price'] = round(max_bom_price / quantity, 3)
+
+            if purchase_price is not None:
+                min_bom_purchase_price, max_bom_purchase_price = purchase_price
+
+                min_bom_purchase_price /= scaler
+                max_bom_purchase_price /= scaler
+                if min_bom_purchase_price:
+                    ctx['min_total_bom_purchase_price'] = round(min_bom_purchase_price, 3)
+                    ctx['min_unit_bom_purchase_price'] = round(min_bom_purchase_price / quantity, 3)
+
+                if max_bom_purchase_price:
+                    ctx['max_total_bom_purchase_price'] = round(max_bom_purchase_price, 3)
+                    ctx['max_unit_bom_purchase_price'] = round(max_bom_purchase_price / quantity, 3)
 
         # internal part pricing information
         internal_part_price = part.get_internal_price(quantity)


Test patch: