From 4bff83108ff85234061910244eb734ff34342dc1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 11 Jul 2026 17:49:28 +1000 Subject: [PATCH] Remove dead pricing code on Part model (#12357) --- src/backend/InvenTree/company/tests.py | 19 -- src/backend/InvenTree/part/models.py | 211 -------------------- src/backend/InvenTree/part/test_bom_item.py | 16 -- src/backend/InvenTree/part/test_part.py | 16 -- 4 files changed, 262 deletions(-) diff --git a/src/backend/InvenTree/company/tests.py b/src/backend/InvenTree/company/tests.py index b5389bfbeb..3187b0ee36 100644 --- a/src/backend/InvenTree/company/tests.py +++ b/src/backend/InvenTree/company/tests.py @@ -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) diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 4ff09c9b6b..3be3681eae 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -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. diff --git a/src/backend/InvenTree/part/test_bom_item.py b/src/backend/InvenTree/part/test_bom_item.py index a6dda90ad1..e72e3442c0 100644 --- a/src/backend/InvenTree/part/test_bom_item.py +++ b/src/backend/InvenTree/part/test_bom_item.py @@ -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 diff --git a/src/backend/InvenTree/part/test_part.py b/src/backend/InvenTree/part/test_part.py index 05e58d225e..a30bddb864 100644 --- a/src/backend/InvenTree/part/test_part.py +++ b/src/backend/InvenTree/part/test_part.py @@ -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]: