Remove dead pricing code on Part model (#12357)
This commit is contained in:
parent
89d11e9e37
commit
4bff83108f
|
|
@ -1,7 +1,6 @@
|
|||
"""Unit tests for the models in the 'company' app."""
|
||||
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
|
@ -99,24 +98,6 @@ class CompanySimpleTest(TestCase):
|
|||
self.assertEqual(p(45), 315)
|
||||
self.assertEqual(p(55), 68.75)
|
||||
|
||||
def test_part_pricing(self):
|
||||
"""Unit tests for supplier part pricing."""
|
||||
m2x4 = Part.objects.get(name='M2x4 LPHS')
|
||||
|
||||
self.assertEqual(m2x4.get_price_info(5.5), '38.5 - 41.25')
|
||||
self.assertEqual(m2x4.get_price_info(10), '70 - 75')
|
||||
self.assertEqual(m2x4.get_price_info(100), '125 - 350')
|
||||
|
||||
pmin, pmax = m2x4.get_price_range(5)
|
||||
self.assertEqual(pmin, 35)
|
||||
self.assertEqual(pmax, 37.5)
|
||||
|
||||
m3x12 = Part.objects.get(name='M3x12 SHCS')
|
||||
|
||||
self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4'))
|
||||
self.assertEqual(m3x12.get_price_info(3), Decimal('24'))
|
||||
self.assertIsNotNone(m3x12.get_price_info(50))
|
||||
|
||||
def test_currency_validation(self):
|
||||
"""Test validation for currency selection."""
|
||||
# Create a company with a valid currency code (should pass)
|
||||
|
|
|
|||
|
|
@ -2214,150 +2214,6 @@ class Part(
|
|||
# which can cause issues down the track
|
||||
pass
|
||||
|
||||
def get_price_info(self, quantity=1, buy=True, bom=True, internal=False):
|
||||
"""Return a simplified pricing string for this part.
|
||||
|
||||
Args:
|
||||
quantity: Number of units to calculate price for
|
||||
buy: Include supplier pricing (default = True)
|
||||
bom: Include BOM pricing (default = True)
|
||||
internal: Include internal pricing (default = False)
|
||||
"""
|
||||
price_range = self.get_price_range(quantity, buy, bom, internal)
|
||||
|
||||
if price_range is None:
|
||||
return None
|
||||
|
||||
min_price, max_price = price_range
|
||||
|
||||
if min_price == max_price:
|
||||
return min_price
|
||||
|
||||
min_price = normalize(min_price)
|
||||
max_price = normalize(max_price)
|
||||
|
||||
return f'{min_price} - {max_price}'
|
||||
|
||||
def get_supplier_price_range(self, quantity=1):
|
||||
"""Return the supplier price range of this part.
|
||||
|
||||
Actions:
|
||||
- Checks if there is any supplier pricing information associated with this Part
|
||||
- Iterate through available supplier pricing and select (min, max)
|
||||
- Returns tuple of (min, max)
|
||||
|
||||
Arguments:
|
||||
quantity: Quantity at which to calculate price (default=1)
|
||||
|
||||
Returns: (min, max) tuple or (None, None) if no supplier pricing available
|
||||
"""
|
||||
min_price = None
|
||||
max_price = None
|
||||
|
||||
for supplier in self.supplier_parts.all():
|
||||
price = supplier.get_price(quantity)
|
||||
|
||||
if price is None:
|
||||
continue
|
||||
|
||||
if min_price is None or price < min_price:
|
||||
min_price = price
|
||||
|
||||
if max_price is None or price > max_price:
|
||||
max_price = price
|
||||
|
||||
if min_price is None or max_price is None:
|
||||
return None
|
||||
|
||||
min_price = normalize(min_price)
|
||||
max_price = normalize(max_price)
|
||||
|
||||
return (min_price, max_price)
|
||||
|
||||
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.
|
||||
Note: If the BOM contains items without pricing information,
|
||||
these items cannot be included in the BOM!
|
||||
"""
|
||||
min_price = None
|
||||
max_price = None
|
||||
|
||||
for item in self.get_bom_items().select_related('sub_part'):
|
||||
if item.sub_part.pk == self.pk:
|
||||
logger.warning('WARNING: BomItem ID %s contains itself in BOM', item.pk)
|
||||
continue
|
||||
|
||||
q = Decimal(quantity)
|
||||
i = Decimal(item.quantity)
|
||||
|
||||
prices = item.sub_part.get_price_range(
|
||||
q * i, internal=internal, purchase=purchase
|
||||
)
|
||||
|
||||
if prices is None:
|
||||
continue
|
||||
|
||||
low, high = prices
|
||||
|
||||
if min_price is None:
|
||||
min_price = 0
|
||||
|
||||
if max_price is None:
|
||||
max_price = 0
|
||||
|
||||
min_price += low
|
||||
max_price += high
|
||||
|
||||
if min_price is None or max_price is None:
|
||||
return None
|
||||
|
||||
min_price = normalize(min_price)
|
||||
max_price = normalize(max_price)
|
||||
|
||||
return (min_price, max_price)
|
||||
|
||||
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, internal or purchase price. If no pricing available, returns None
|
||||
"""
|
||||
# only get internal price if set and should be used
|
||||
if internal and self.has_internal_price_breaks:
|
||||
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
|
||||
)
|
||||
|
||||
if buy_price_range is None:
|
||||
return bom_price_range
|
||||
|
||||
elif bom_price_range is None:
|
||||
return buy_price_range
|
||||
return (
|
||||
min(buy_price_range[0], bom_price_range[0]),
|
||||
max(buy_price_range[1], bom_price_range[1]),
|
||||
)
|
||||
|
||||
base_cost = models.DecimalField(
|
||||
max_digits=19,
|
||||
decimal_places=6,
|
||||
|
|
@ -2374,73 +2230,6 @@ class Part(
|
|||
help_text=_('Sell multiple'),
|
||||
)
|
||||
|
||||
get_price = common.currency.get_price
|
||||
|
||||
@property
|
||||
def has_price_breaks(self):
|
||||
"""Return True if this part has sale price breaks."""
|
||||
return self.price_breaks.exists()
|
||||
|
||||
@property
|
||||
def price_breaks(self):
|
||||
"""Return the associated price breaks in the correct order."""
|
||||
return self.salepricebreaks.order_by('quantity').all()
|
||||
|
||||
@property
|
||||
def unit_pricing(self):
|
||||
"""Returns the price of this Part at quantity=1."""
|
||||
return self.get_price(1)
|
||||
|
||||
def add_price_break(self, quantity, price):
|
||||
"""Create a new price break for this part.
|
||||
|
||||
Args:
|
||||
quantity: Numerical quantity
|
||||
price: Must be a Money object
|
||||
"""
|
||||
# Check if a price break at that quantity already exists...
|
||||
if self.price_breaks.filter(quantity=quantity, part=self.pk).exists():
|
||||
return
|
||||
|
||||
PartSellPriceBreak.objects.create(part=self, quantity=quantity, price=price)
|
||||
|
||||
def get_internal_price(self, quantity, moq=True, multiples=True, currency=None):
|
||||
"""Return the internal price of this Part at the specified quantity."""
|
||||
return common.currency.get_price(
|
||||
self, quantity, moq, multiples, currency, break_name='internal_price_breaks'
|
||||
)
|
||||
|
||||
@property
|
||||
def has_internal_price_breaks(self):
|
||||
"""Return True if this Part has internal pricing information."""
|
||||
return self.internal_price_breaks.exists()
|
||||
|
||||
@property
|
||||
def internal_price_breaks(self):
|
||||
"""Return the associated price breaks in the correct order."""
|
||||
return self.internalpricebreaks.order_by('quantity').all()
|
||||
|
||||
def get_purchase_price(self, quantity):
|
||||
"""Calculate the purchase price for this part at the specified quantity.
|
||||
|
||||
- Looks at available supplier pricing data
|
||||
- Calculates the price base on the closest price point
|
||||
"""
|
||||
currency = currency_code_default()
|
||||
try:
|
||||
prices = [
|
||||
convert_money(item.purchase_price, currency).amount
|
||||
for item in self.stock_items.all()
|
||||
if item.purchase_price
|
||||
]
|
||||
except MissingRate:
|
||||
prices = None
|
||||
|
||||
if prices:
|
||||
return min(prices) * quantity, max(prices) * quantity
|
||||
|
||||
return None
|
||||
|
||||
@transaction.atomic
|
||||
def copy_bom_from(self, other, clear: bool = True, **kwargs):
|
||||
"""Copy the BOM from another part.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Unit tests for the BomItem model."""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import django.core.exceptions as django_exceptions
|
||||
from django.db import transaction
|
||||
from django.test import TestCase
|
||||
|
|
@ -171,20 +169,6 @@ class BomItemTest(TestCase):
|
|||
|
||||
self.assertNotEqual(h1, h2)
|
||||
|
||||
def test_pricing(self):
|
||||
"""Test BOM pricing."""
|
||||
self.bob.get_price(1)
|
||||
self.assertEqual(
|
||||
self.bob.get_bom_price_range(1, internal=True),
|
||||
(Decimal(29.5), Decimal(89.5)),
|
||||
)
|
||||
# remove internal price for R_2K2_0805
|
||||
self.r1.internal_price_breaks.delete()
|
||||
self.assertEqual(
|
||||
self.bob.get_bom_price_range(1, internal=True),
|
||||
(Decimal(27.5), Decimal(87.5)),
|
||||
)
|
||||
|
||||
def test_substitutes(self):
|
||||
"""Tests for BOM item substitutes."""
|
||||
# We will make some substitute parts for the "orphan" part
|
||||
|
|
|
|||
|
|
@ -250,22 +250,6 @@ class PartTest(TestCase):
|
|||
barcode = self.r1.format_barcode()
|
||||
self.assertEqual('INV-PA3', barcode)
|
||||
|
||||
def test_sell_pricing(self):
|
||||
"""Check that the sell pricebreaks were loaded."""
|
||||
self.assertTrue(self.r1.has_price_breaks)
|
||||
self.assertEqual(self.r1.price_breaks.count(), 2)
|
||||
# check that the sell pricebreaks work
|
||||
self.assertEqual(float(self.r1.get_price(1)), 0.15)
|
||||
self.assertEqual(float(self.r1.get_price(10)), 1.0)
|
||||
|
||||
def test_internal_pricing(self):
|
||||
"""Check that the sell pricebreaks were loaded."""
|
||||
self.assertTrue(self.r1.has_internal_price_breaks)
|
||||
self.assertEqual(self.r1.internal_price_breaks.count(), 2)
|
||||
# check that the sell pricebreaks work
|
||||
self.assertEqual(float(self.r1.get_internal_price(1)), 0.08)
|
||||
self.assertEqual(float(self.r1.get_internal_price(10)), 0.5)
|
||||
|
||||
def test_metadata(self):
|
||||
"""Unit tests for the metadata field."""
|
||||
for model in [Part]:
|
||||
|
|
|
|||
Loading…
Reference in New Issue