mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 20:45:44 +00:00
Clean out existing pricing functions
This commit is contained in:
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import decimal
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -44,7 +43,7 @@ from common.settings import currency_code_default
|
|||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
from InvenTree import helpers, validators
|
from InvenTree import helpers, validators
|
||||||
from InvenTree.fields import InvenTreeNotesField, InvenTreeURLField
|
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,
|
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
|
||||||
InvenTreeBarcodeMixin, InvenTreeTree)
|
InvenTreeBarcodeMixin, InvenTreeTree)
|
||||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
||||||
@ -1667,119 +1666,6 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
|
|||||||
except (PartPricing.DoesNotExist, IntegrityError):
|
except (PartPricing.DoesNotExist, IntegrityError):
|
||||||
pass
|
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)'))
|
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'))
|
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 the associated price breaks in the correct order."""
|
||||||
return self.internalpricebreaks.order_by('quantity').all()
|
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
|
@transaction.atomic
|
||||||
def copy_bom_from(self, other, clear=True, **kwargs):
|
def copy_bom_from(self, other, clear=True, **kwargs):
|
||||||
"""Copy the BOM from another part.
|
"""Copy the BOM from another part.
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
|
|
||||||
"""Unit tests for the BomItem model"""
|
"""Unit tests for the BomItem model"""
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
import django.core.exceptions as django_exceptions
|
import django.core.exceptions as django_exceptions
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@ -117,20 +115,6 @@ class BomItemTest(TestCase):
|
|||||||
|
|
||||||
self.assertNotEqual(h1, h2)
|
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):
|
def test_substitutes(self):
|
||||||
"""Tests for BOM item substitutes."""
|
"""Tests for BOM item substitutes."""
|
||||||
# We will make some subtitute parts for the "orphan" part
|
# We will make some subtitute parts for the "orphan" part
|
||||||
|
Reference in New Issue
Block a user