mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Clean out existing pricing functions
This commit is contained in:
		@@ -2,7 +2,6 @@
 | 
			
		||||
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import decimal
 | 
			
		||||
import hashlib
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
@@ -44,7 +43,7 @@ from common.settings import currency_code_default
 | 
			
		||||
from company.models import SupplierPart
 | 
			
		||||
from InvenTree import helpers, validators
 | 
			
		||||
from InvenTree.fields import InvenTreeNotesField, InvenTreeURLField
 | 
			
		||||
from InvenTree.helpers import decimal2money, decimal2string, normalize
 | 
			
		||||
from InvenTree.helpers import decimal2money, decimal2string
 | 
			
		||||
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
 | 
			
		||||
                              InvenTreeBarcodeMixin, InvenTreeTree)
 | 
			
		||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
 | 
			
		||||
@@ -1667,119 +1666,6 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
 | 
			
		||||
        except (PartPricing.DoesNotExist, IntegrityError):
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    def get_supplier_price_range(self, quantity=1):
 | 
			
		||||
        """Return the supplier price range of this part:
 | 
			
		||||
 | 
			
		||||
        - 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(f"WARNING: BomItem ID {item.pk} contains itself in BOM")
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            q = decimal.Decimal(quantity)
 | 
			
		||||
            i = decimal.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 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
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            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, default=0, validators=[MinValueValidator(0)], verbose_name=_('base cost'), help_text=_('Minimum charge (e.g. stocking fee)'))
 | 
			
		||||
 | 
			
		||||
    multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], verbose_name=_('multiple'), help_text=_('Sell multiple'))
 | 
			
		||||
@@ -1828,23 +1714,6 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
 | 
			
		||||
        """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=True, **kwargs):
 | 
			
		||||
        """Copy the BOM from another part.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
 | 
			
		||||
"""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
 | 
			
		||||
@@ -117,20 +115,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 subtitute parts for the "orphan" part
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user