2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +00:00

Include stock item purchase price in pricing cache (#4292)

* Add setting to control pricing calculation from stock items

* Bug fix for displaying a "null" setting

* Add new fields to PartPricing model

* Add code for calculation of min/max stock item costs

* Update pricing display to use stock item pricing

* Add unit testing for new pricing features

* Automatically update pricing when stock item is created or edited

* Increment API version

* Improvements for price rendering

* Update based on feedback:

- Roll stock item pricing into purchase pricing
- Simplify models
- Update unit tests
This commit is contained in:
Oliver
2023-02-02 21:23:16 +11:00
committed by GitHub
parent df4209801a
commit 9a289948e5
10 changed files with 126 additions and 42 deletions

View File

@ -6,7 +6,7 @@ import decimal
import hashlib
import logging
import os
from datetime import datetime
from datetime import datetime, timedelta
from decimal import Decimal, InvalidOperation
from django.contrib.auth.models import User
@ -2510,6 +2510,31 @@ class PartPricing(common.models.MetaMixin):
if purchase_max is None or purchase_cost > purchase_max:
purchase_max = purchase_cost
# Also check if manual stock item pricing is included
if InvenTreeSetting.get_setting('PRICING_USE_STOCK_PRICING', True, cache=False):
items = self.part.stock_items.all()
# Limit to stock items updated within a certain window
days = int(InvenTreeSetting.get_setting('PRICING_STOCK_ITEM_AGE_DAYS', 0, cache=False))
if days > 0:
date_threshold = datetime.now().date() - timedelta(days=days)
items = items.filter(updated__gte=date_threshold)
for item in items:
cost = self.convert(item.purchase_price)
# Skip if the cost could not be converted (for some reason)
if cost is None:
continue
if purchase_min is None or cost < purchase_min:
purchase_min = cost
if purchase_max is None or cost > purchase_max:
purchase_max = cost
self.purchase_cost_min = purchase_min
self.purchase_cost_max = purchase_max
@ -2651,6 +2676,7 @@ class PartPricing(common.models.MetaMixin):
max_costs.append(self.supplier_price_max)
if InvenTreeSetting.get_setting('PRICING_USE_VARIANT_PRICING', True, cache=False):
# Include variant pricing in overall calculations
min_costs.append(self.variant_cost_min)
max_costs.append(self.variant_cost_max)

View File

@ -45,9 +45,7 @@
</a>
{% endif %}
</td>
<th>
{% trans "Internal Pricing" %}
</th>
<th>{% trans "Internal Pricing" %}</th>
<td>{% include "price_data.html" with price=pricing.internal_cost_min %}</td>
<td>{% include "price_data.html" with price=pricing.internal_cost_max %}</td>
</tr>
@ -60,9 +58,7 @@
</a>
{% endif %}
</td>
<th>
{% trans "Purchase History" %}
</th>
<th>{% trans "Purchase History" %}</th>
<td>{% include "price_data.html" with price=pricing.purchase_cost_min %}</td>
<td>{% include "price_data.html" with price=pricing.purchase_cost_max %}</td>
</tr>
@ -74,9 +70,7 @@
</a>
{% endif %}
</td>
<th>
{% trans "Supplier Pricing" %}
</th>
<th>{% trans "Supplier Pricing" %}</th>
<td>{% include "price_data.html" with price=pricing.supplier_price_min %}</td>
<td>{% include "price_data.html" with price=pricing.supplier_price_max %}</td>
</tr>
@ -90,9 +84,7 @@
</a>
{% endif %}
</td>
<th>
{% trans "BOM Pricing" %}
</th>
<th>{% trans "BOM Pricing" %}</th>
<td>{% include "price_data.html" with price=pricing.bom_cost_min %}</td>
<td>{% include "price_data.html" with price=pricing.bom_cost_max %}</td>
</tr>
@ -107,9 +99,7 @@
{% endif %}
<tr>
<td></td>
<th>
{% trans "Overall Pricing" %}
</th>
<th>{% trans "Overall Pricing" %}</th>
<th>{% include "price_data.html" with price=pricing.overall_min %}</th>
<th>{% include "price_data.html" with price=pricing.overall_max %}</th>
</tr>
@ -135,15 +125,9 @@
<span class='fas fa-dollar-sign'></span>
</a>
</td>
<th>
{% trans "Sale Price" %}
</th>
<td>
{% include "price_data.html" with price=pricing.sale_price_min %}
</td>
<td>
{% include "price_data.html" with price=pricing.sale_price_max %}
</td>
<th>{% trans "Sale Price" %}</th>
<td>{% include "price_data.html" with price=pricing.sale_price_min %}</td>
<td>{% include "price_data.html" with price=pricing.sale_price_max %}</td>
</tr>
<tr>
<td>
@ -151,15 +135,9 @@
<span class='fas fa-chart-line'></span>
</a>
</td>
<th>
{% trans "Sale History" %}
</th>
<td>
{% include "price_data.html" with price=pricing.sale_history_min %}
</td>
<td>
{% include "price_data.html" with price=pricing.sale_history_max %}
</td>
<th>{% trans "Sale History" %}</th>
<td>{% include "price_data.html" with price=pricing.sale_history_min %}</td>
<td>{% include "price_data.html" with price=pricing.sale_history_max %}</td>
</tr>
</tbody>
</table>

View File

@ -10,6 +10,7 @@ import common.settings
import company.models
import order.models
import part.models
import stock.models
from InvenTree.helpers import InvenTreeTestCase
from InvenTree.status_codes import PurchaseOrderStatus
@ -234,6 +235,54 @@ class PartPricingTests(InvenTreeTestCase):
self.assertEqual(pricing.internal_cost_max, Money(10, currency))
self.assertEqual(pricing.overall_max, Money(10, currency))
def test_stock_item_pricing(self):
"""Test for stock item pricing data"""
# Create a part
p = part.models.Part.objects.create(
name='Test part for pricing',
description='hello world',
)
# Create some stock items
prices = [
(10, 'AUD'),
(5, 'USD'),
(2, 'CAD'),
]
for price, currency in prices:
stock.models.StockItem.objects.create(
part=p,
quantity=10,
purchase_price=price,
purchase_price_currency=currency
)
# Ensure that initially, stock item pricing is disabled
common.models.InvenTreeSetting.set_setting('PRICING_USE_STOCK_PRICING', False, None)
pricing = p.pricing
pricing.update_pricing()
# Check that stock item pricing data is not used
self.assertIsNone(pricing.purchase_cost_min)
self.assertIsNone(pricing.purchase_cost_max)
self.assertIsNone(pricing.overall_min)
self.assertIsNone(pricing.overall_max)
# Turn on stock pricing
common.models.InvenTreeSetting.set_setting('PRICING_USE_STOCK_PRICING', True, None)
pricing.update_pricing()
self.assertIsNotNone(pricing.purchase_cost_min)
self.assertIsNotNone(pricing.purchase_cost_max)
self.assertEqual(pricing.overall_min, Money(1.176471, 'USD'))
self.assertEqual(pricing.overall_max, Money(6.666667, 'USD'))
def test_bom_pricing(self):
"""Unit test for BOM pricing calculations"""